├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── README_cn.md ├── README_en.md ├── api ├── comet │ ├── comet.pb.go │ └── comet.proto ├── generate.go ├── logic │ ├── logic.pb.go │ └── logic.proto └── protocol │ ├── operation.go │ ├── protocol.go │ ├── protocol.pb.go │ └── protocol.proto ├── benchmarks ├── client │ └── main.go ├── multi_push │ └── main.go ├── push │ └── main.go ├── push_room │ └── main.go └── push_rooms │ └── main.go ├── cmd ├── comet │ ├── comet-example.toml │ └── main.go ├── job │ ├── job-example.toml │ └── main.go └── logic │ ├── logic-example.toml │ └── main.go ├── codecov.sh ├── docs ├── arch.png ├── benchmark-comet.jpg ├── benchmark-flow.jpg ├── benchmark-heap.jpg ├── benchmark.jpg ├── benchmark_cn.md ├── benchmark_en.md ├── en │ ├── proto.md │ └── push.md ├── goim.graffle ├── handshake.png ├── proto.md ├── protocol.png └── push.md ├── examples ├── cert.pem ├── javascript │ ├── client.js │ ├── index.html │ └── main.go └── private.pem ├── go.mod ├── go.sum ├── internal ├── comet │ ├── bucket.go │ ├── channel.go │ ├── conf │ │ └── conf.go │ ├── errors │ │ └── errors.go │ ├── grpc │ │ └── server.go │ ├── operation.go │ ├── ring.go │ ├── room.go │ ├── round.go │ ├── server.go │ ├── server_tcp.go │ ├── server_websocket.go │ └── whitelist.go ├── job │ ├── comet.go │ ├── conf │ │ └── conf.go │ ├── job.go │ ├── push.go │ └── room.go └── logic │ ├── balancer.go │ ├── balancer_test.go │ ├── conf │ └── conf.go │ ├── conn.go │ ├── conn_test.go │ ├── dao │ ├── dao.go │ ├── dao_test.go │ ├── kafka.go │ ├── kafka_test.go │ ├── redis.go │ ├── redis_test.go │ └── size_coverage.out │ ├── grpc │ └── server.go │ ├── http │ ├── middleware.go │ ├── nodes.go │ ├── online.go │ ├── push.go │ ├── result.go │ └── server.go │ ├── logic.go │ ├── logic_test.go │ ├── model │ ├── metadata.go │ ├── online.go │ └── room.go │ ├── nodes.go │ ├── nodes_test.go │ ├── online.go │ ├── online_test.go │ ├── push.go │ └── push_test.go ├── pkg ├── bufio │ ├── bufio.go │ └── bufio_test.go ├── bytes │ ├── buffer.go │ ├── buffer_test.go │ ├── writer.go │ └── writer_test.go ├── encoding │ └── binary │ │ ├── endian.go │ │ └── endian_test.go ├── ip │ ├── ip.go │ └── ip_test.go ├── strings │ ├── ints.go │ └── ints_test.go ├── time │ ├── debug.go │ ├── duration.go │ ├── duration_test.go │ ├── timer.go │ └── timer_test.go └── websocket │ ├── conn.go │ ├── request.go │ ├── server.go │ └── server_test.go └── scripts ├── README.md ├── jdk8.sh ├── kafka.sh └── zk.sh /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE ignore 2 | .idea/ 3 | *.ipr 4 | *.iml 5 | *.iws 6 | .vscode/ 7 | 8 | # temp ignore 9 | *.log 10 | *.cache 11 | *.diff 12 | *.exe 13 | *.exe~ 14 | *.patch 15 | *.tmp 16 | *.swp 17 | 18 | # system ignore 19 | .DS_Store 20 | Thumbs.db 21 | 22 | # build 23 | /cmd/comet/comet 24 | /cmd/logic/logic 25 | /cmd/job/job 26 | /target 27 | /configs 28 | /dist 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### goim 2 | 3 | ##### Version 2.0.0 4 | > 1.router has been changed to redis 5 | > 2.Support node with redis online heartbeat maintenance 6 | > 3.Support for gRPC and Discovery services 7 | > 4.Support node connection number and weight scheduling 8 | > 5.Support node scheduling by region 9 | > 6.Support instruction subscription 10 | > 7.Support the current connection room switch 11 | > 8.Support multiple room types ({type}://{room_id}) 12 | > 9.Support sending messages by device_id 13 | > 10.Support for room message aggregation 14 | > 11.Supports IPv6 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Terry.Mao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=GO111MODULE=on go 3 | GOBUILD=$(GOCMD) build 4 | GOTEST=$(GOCMD) test 5 | 6 | all: test build 7 | build: 8 | rm -rf target/ 9 | mkdir target/ 10 | cp cmd/comet/comet-example.toml target/comet.toml 11 | cp cmd/logic/logic-example.toml target/logic.toml 12 | cp cmd/job/job-example.toml target/job.toml 13 | $(GOBUILD) -o target/comet cmd/comet/main.go 14 | $(GOBUILD) -o target/logic cmd/logic/main.go 15 | $(GOBUILD) -o target/job cmd/job/main.go 16 | 17 | test: 18 | $(GOTEST) -v ./... 19 | 20 | clean: 21 | rm -rf target/ 22 | 23 | run: 24 | nohup target/logic -conf=target/logic.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 2>&1 > target/logic.log & 25 | nohup target/comet -conf=target/comet.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 -addrs=127.0.0.1 -debug=true 2>&1 > target/comet.log & 26 | nohup target/job -conf=target/job.toml -region=sh -zone=sh001 -deploy.env=dev 2>&1 > target/job.log & 27 | 28 | stop: 29 | pkill -f target/logic 30 | pkill -f target/job 31 | pkill -f target/comet 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goim v2.0 2 | ============== 3 | 4 | [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) 5 | [![Build Status](https://github.com/Terry-Mao/goim/workflows/Go/badge.svg)](https://github.com/Terry-Mao/goim/actions) 6 | [![GoDoc](https://godoc.org/github.com/go-kratos/kratos?status.svg)](https://pkg.go.dev/github.com/Terry-Mao/goim) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/Terry-Mao/goim)](https://goreportcard.com/report/github.com/Terry-Mao/goim) 8 | 9 | goim is an im server writen in golang. 10 | 11 | ## Features 12 | * Light weight 13 | * High performance 14 | * Pure Golang 15 | * Supports single push, multiple push and broadcasting 16 | * Supports one key to multiple subscribers (Configurable maximum subscribers count) 17 | * Supports heartbeats (Application heartbeats, TCP, KeepAlive, HTTP long pulling) 18 | * Supports authentication (Unauthenticated user can't subscribe) 19 | * Supports multiple protocols (WebSocket,TCP,HTTP) 20 | * Scalable architecture (Unlimited dynamic job and logic modules) 21 | * Asynchronous push notification based on Kafka 22 | 23 | ## Architecture 24 | ![arch](./docs/arch.png) 25 | 26 | ## Quick Start 27 | 28 | ### Build 29 | ``` 30 | make build 31 | ``` 32 | 33 | ### Run 34 | ``` 35 | make run 36 | make stop 37 | 38 | // or 39 | nohup target/logic -conf=target/logic.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 2>&1 > target/logic.log & 40 | nohup target/comet -conf=target/comet.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 -addrs=127.0.0.1 2>&1 > target/logic.log & 41 | nohup target/job -conf=target/job.toml -region=sh -zone=sh001 -deploy.env=dev 2>&1 > target/logic.log & 42 | 43 | ``` 44 | ### Environment 45 | ``` 46 | env: 47 | export REGION=sh 48 | export ZONE=sh001 49 | export DEPLOY_ENV=dev 50 | 51 | supervisor: 52 | environment=REGION=sh,ZONE=sh001,DEPLOY_ENV=dev 53 | 54 | go flag: 55 | -region=sh -zone=sh001 deploy.env=dev 56 | ``` 57 | ### Configuration 58 | You can view the comments in target/comet.toml,logic.toml,job.toml to understand the meaning of the config. 59 | 60 | ### Dependencies 61 | [Discovery](https://github.com/bilibili/discovery) 62 | 63 | [Kafka](https://kafka.apache.org/quickstart) 64 | 65 | ## Document 66 | [Protocol](./docs/protocol.png) 67 | 68 | [English](./README_en.md) 69 | 70 | [中文](./README_cn.md) 71 | 72 | ## Examples 73 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript) 74 | 75 | Android: [Android](https://github.com/roamdy/goim-sdk) 76 | 77 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk) 78 | 79 | ## Benchmark 80 | ![benchmark](./docs/benchmark.jpg) 81 | 82 | ### Benchmark Server 83 | | CPU | Memory | OS | Instance | 84 | | :---- | :---- | :---- | :---- | 85 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 | 86 | 87 | ### Benchmark Case 88 | * Online: 1,000,000 89 | * Duration: 15min 90 | * Push Speed: 40/s (broadcast room) 91 | * Push Message: {"test":1} 92 | * Received calc mode: 1s per times, total 30 times 93 | 94 | ### Benchmark Resource 95 | * CPU: 2000%~2300% 96 | * Memory: 14GB 97 | * GC Pause: 504ms 98 | * Network: Incoming(450MBit/s), Outgoing(4.39GBit/s) 99 | 100 | ### Benchmark Result 101 | * Received: 35,900,000/s 102 | 103 | [中文](./docs/benchmark_cn.md) 104 | 105 | [English](./docs/benchmark_en.md) 106 | 107 | ## LICENSE 108 | goim is is distributed under the terms of the MIT License. 109 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | goim v2.0 2 | ============== 3 | `Terry-Mao/goim` 是一个支持集群的im及实时推送服务。 4 | 5 | --------------------------------------- 6 | * [特性](#特性) 7 | * [安装](#安装) 8 | * [配置](#配置) 9 | * [例子](#例子) 10 | * [文档](#文档) 11 | * [集群](#集群) 12 | * [更多](#更多) 13 | 14 | --------------------------------------- 15 | 16 | ## 特性 17 | * 轻量级 18 | * 高性能 19 | * 纯Golang实现 20 | * 支持单个、多个、单房间以及广播消息推送 21 | * 支持单个Key多个订阅者(可限制订阅者最大人数) 22 | * 心跳支持(应用心跳和tcp、keepalive) 23 | * 支持安全验证(未授权用户不能订阅) 24 | * 多协议支持(websocket,tcp) 25 | * 可拓扑的架构(job、logic模块可动态无限扩展) 26 | * 基于Kafka做异步消息推送 27 | 28 | ## 安装 29 | ### 一、安装依赖 30 | ```sh 31 | $ yum -y install java-1.7.0-openjdk 32 | ``` 33 | 34 | ### 二、安装Kafka消息队列服务 35 | 36 | kafka在官网已经描述的非常详细,在这里就不过多说明,安装、启动请查看[这里](http://kafka.apache.org/documentation.html#quickstart). 37 | 38 | ### 三、搭建golang环境 39 | 1.下载源码(根据自己的系统下载对应的[安装包](http://golang.org/dl/)) 40 | ```sh 41 | $ cd /data/programfiles 42 | $ wget -c --no-check-certificate https://storage.googleapis.com/golang/go1.5.2.linux-amd64.tar.gz 43 | $ tar -xvf go1.5.2.linux-amd64.tar.gz -C /usr/local 44 | ``` 45 | 2.配置GO环境变量 46 | (这里我加在/etc/profile.d/golang.sh) 47 | ```sh 48 | $ vi /etc/profile.d/golang.sh 49 | # 将以下环境变量添加到profile最后面 50 | export GOROOT=/usr/local/go 51 | export PATH=$PATH:$GOROOT/bin 52 | export GOPATH=/data/apps/go 53 | $ source /etc/profile 54 | ``` 55 | 56 | ### 四、部署goim 57 | 1.下载goim及依赖包 58 | ```sh 59 | $ yum install hg 60 | $ go get -u github.com/Terry-Mao/goim 61 | $ mv $GOPATH/src/github.com/Terry-Mao/goim $GOPATH/src/goim 62 | $ cd $GOPATH/src/goim 63 | $ go get ./... 64 | ``` 65 | 66 | 2.安装router、logic、comet、job模块(配置文件请依据实际机器环境配置) 67 | ```sh 68 | $ cd $GOPATH/src/goim/router 69 | $ go install 70 | $ cp router-example.conf $GOPATH/bin/router.conf 71 | $ cp router-log.xml $GOPATH/bin/ 72 | $ cd ../logic/ 73 | $ go install 74 | $ cp logic-example.conf $GOPATH/bin/logic.conf 75 | $ cp logic-log.xml $GOPATH/bin/ 76 | $ cd ../comet/ 77 | $ go install 78 | $ cp comet-example.conf $GOPATH/bin/comet.conf 79 | $ cp comet-log.xml $GOPATH/bin/ 80 | $ cd ../logic/job/ 81 | $ go install 82 | $ cp job-example.conf $GOPATH/bin/job.conf 83 | $ cp job-log.xml $GOPATH/bin/ 84 | ``` 85 | 到此所有的环境都搭建完成! 86 | 87 | ### 五、启动goim 88 | ```sh 89 | $ cd /$GOPATH/bin 90 | $ nohup $GOPATH/bin/router -c $GOPATH/bin/router.conf 2>&1 > /data/logs/goim/panic-router.log & 91 | $ nohup $GOPATH/bin/logic -c $GOPATH/bin/logic.conf 2>&1 > /data/logs/goim/panic-logic.log & 92 | $ nohup $GOPATH/bin/comet -c $GOPATH/bin/comet.conf 2>&1 > /data/logs/goim/panic-comet.log & 93 | $ nohup $GOPATH/bin/job -c $GOPATH/bin/job.conf 2>&1 > /data/logs/goim/panic-job.log & 94 | ``` 95 | 如果启动失败,默认配置可通过查看panic-xxx.log日志文件来排查各个模块问题. 96 | 97 | ### 六、测试 98 | 99 | 推送协议可查看[push http协议文档](./docs/push.md) 100 | 101 | ## 配置 102 | 103 | TODO 104 | 105 | ## 例子 106 | 107 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript) 108 | 109 | Android: [Android](https://github.com/roamdy/goim-sdk) 110 | 111 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk) 112 | 113 | ## 文档 114 | [push http协议文档](./docs/push.md)推送接口 115 | 116 | ## 集群 117 | 118 | ### comet 119 | 120 | comet 属于接入层,非常容易扩展,直接开启多个comet节点,修改配置文件中的base节点下的server.id修改成不同值(注意一定要保证不同的comet进程值唯一),前端接入可以使用LVS 或者 DNS来转发 121 | 122 | ### logic 123 | 124 | logic 属于无状态的逻辑层,可以随意增加节点,使用nginx upstream来扩展http接口,内部rpc部分,可以使用LVS四层转发 125 | 126 | ### kafka 127 | 128 | kafka 可以使用多broker,或者多partition来扩展队列 129 | 130 | ### router 131 | 132 | router 属于有状态节点,logic可以使用一致性hash配置节点,增加多个router节点(目前还不支持动态扩容),提前预估好在线和压力情况 133 | 134 | ### job 135 | 136 | job 根据kafka的partition来扩展多job工作方式,具体可以参考下kafka的partition负载 137 | 138 | ## 更多 139 | TODO 140 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | goim 2 | ============== 3 | `Terry-Mao/goim` is a IM and push notification server cluster. 4 | 5 | --------------------------------------- 6 | * [Features](#features) 7 | * [Installing](#installing) 8 | * [Configurations](#configurations) 9 | * [Examples](#examples) 10 | * [Documents](#documents) 11 | * [More](#more) 12 | 13 | --------------------------------------- 14 | 15 | ## Features 16 | * Light weight 17 | * High performance 18 | * Pure Golang 19 | * Supports single push, multiple push, room push and broadcasting 20 | * Supports one key to multiple subscribers (Configurable maximum subscribers count) 21 | * Supports heartbeats (Application heartbeats, TCP, KeepAlive) 22 | * Supports authentication (Unauthenticated user can't subscribe) 23 | * Supports multiple protocols (WebSocket,TCP) 24 | * Scalable architecture (Unlimited dynamic job and logic modules) 25 | * Asynchronous push notification based on Kafka 26 | 27 | ## Installing 28 | ### Dependencies 29 | ```sh 30 | $ yum -y install java-1.7.0-openjdk 31 | ``` 32 | 33 | ### Install Kafka 34 | 35 | Please follow the official quick start [here](http://kafka.apache.org/documentation.html#quickstart). 36 | 37 | ### Install Golang environment 38 | 39 | Please follow the official quick start [here](https://golang.org/doc/install). 40 | 41 | ### Deploy goim 42 | 1.Download goim 43 | ```sh 44 | $ yum install git 45 | $ cd $GOPATH/src 46 | $ git clone https://github.com/Terry-Mao/goim.git 47 | $ cd $GOPATH/src/goim 48 | $ go get ./... 49 | ``` 50 | 51 | 2.Install router、logic、comet、job modules(You might need to change the configuration files based on your servers) 52 | ```sh 53 | $ cd $GOPATH/src/goim/router 54 | $ go install 55 | $ cp router-example.conf $GOPATH/bin/router.conf 56 | $ cp router-log.xml $GOPATH/bin/ 57 | $ cd ../logic/ 58 | $ go install 59 | $ cp logic-example.conf $GOPATH/bin/logic.conf 60 | $ cp logic-log.xml $GOPATH/bin/ 61 | $ cd ../comet/ 62 | $ go install 63 | $ cp comet-example.conf $GOPATH/bin/comet.conf 64 | $ cp comet-log.xml $GOPATH/bin/ 65 | $ cd ../logic/job/ 66 | $ go install 67 | $ cp job-example.conf $GOPATH/bin/job.conf 68 | $ cp job-log.xml $GOPATH/bin/ 69 | ``` 70 | 71 | Everything is DONE! 72 | 73 | ### Run goim 74 | You may need to change the log files location. 75 | ```sh 76 | $ cd /$GOPATH/bin 77 | $ nohup $GOPATH/bin/router -c $GOPATH/bin/router.conf 2>&1 > /data/logs/goim/panic-router.log & 78 | $ nohup $GOPATH/bin/logic -c $GOPATH/bin/logic.conf 2>&1 > /data/logs/goim/panic-logic.log & 79 | $ nohup $GOPATH/bin/comet -c $GOPATH/bin/comet.conf 2>&1 > /data/logs/goim/panic-comet.log & 80 | $ nohup $GOPATH/bin/job -c $GOPATH/bin/job.conf 2>&1 > /data/logs/goim/panic-job.log & 81 | ``` 82 | 83 | If it fails, please check the logs for debugging. 84 | 85 | ### Testing 86 | 87 | Check the push protocols here[push HTTP protocols](./docs/push.md) 88 | 89 | ## Configurations 90 | TODO 91 | 92 | ## Examples 93 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript) 94 | 95 | Android: [Android SDK](https://github.com/roamdy/goim-sdk) 96 | 97 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk) 98 | 99 | ## Documents 100 | [push HTTP protocols](./docs/en/push.md) 101 | 102 | [Comet client protocols](./docs/en/proto.md) 103 | 104 | ##More 105 | TODO 106 | -------------------------------------------------------------------------------- /api/comet/comet.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package goim.comet; 4 | 5 | option go_package = "github.com/Terry-Mao/goim/api/comet;comet"; 6 | 7 | import "github.com/Terry-Mao/goim/api/protocol/protocol.proto"; 8 | 9 | message PushMsgReq { 10 | repeated string keys = 1; 11 | int32 protoOp = 3; 12 | goim.protocol.Proto proto = 2; 13 | } 14 | 15 | message PushMsgReply {} 16 | 17 | message BroadcastReq{ 18 | int32 protoOp = 1; 19 | goim.protocol.Proto proto = 2; 20 | int32 speed = 3; 21 | } 22 | 23 | message BroadcastReply{} 24 | 25 | message BroadcastRoomReq { 26 | string roomID = 1; 27 | goim.protocol.Proto proto = 2; 28 | } 29 | 30 | message BroadcastRoomReply{} 31 | 32 | message RoomsReq{} 33 | 34 | message RoomsReply { 35 | map rooms = 1; 36 | } 37 | 38 | service Comet { 39 | // PushMsg push by key or mid 40 | rpc PushMsg(PushMsgReq) returns (PushMsgReply); 41 | // Broadcast send to every enrity 42 | rpc Broadcast(BroadcastReq) returns (BroadcastReply); 43 | // BroadcastRoom broadcast to one room 44 | rpc BroadcastRoom(BroadcastRoomReq) returns (BroadcastRoomReply); 45 | // Rooms get all rooms 46 | rpc Rooms(RoomsReq) returns (RoomsReply); 47 | } 48 | -------------------------------------------------------------------------------- /api/generate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative protocol/protocol.proto 4 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative comet/comet.proto 5 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative logic/logic.proto 6 | -------------------------------------------------------------------------------- /api/logic/logic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package goim.logic; 4 | 5 | option go_package = "github.com/Terry-Mao/goim/api/logic;logic"; 6 | 7 | import "github.com/Terry-Mao/goim/api/protocol/protocol.proto"; 8 | 9 | message PushMsg { 10 | enum Type { 11 | PUSH = 0; 12 | ROOM = 1; 13 | BROADCAST = 2; 14 | } 15 | Type type = 1; 16 | int32 operation = 2; 17 | int32 speed = 3; 18 | string server = 4; 19 | string room = 5; 20 | repeated string keys = 6; 21 | bytes msg = 7; 22 | } 23 | 24 | message ConnectReq { 25 | string server = 1; 26 | string cookie = 2; 27 | bytes token = 3; 28 | } 29 | 30 | message ConnectReply { 31 | int64 mid = 1; 32 | string key = 2; 33 | string roomID = 3; 34 | repeated int32 accepts = 4; 35 | int64 heartbeat = 5; 36 | } 37 | 38 | message DisconnectReq { 39 | int64 mid = 1; 40 | string key = 2; 41 | string server = 3; 42 | } 43 | 44 | message DisconnectReply { 45 | bool has = 1; 46 | } 47 | 48 | message HeartbeatReq { 49 | int64 mid = 1; 50 | string key = 2; 51 | string server = 3; 52 | } 53 | 54 | message HeartbeatReply { 55 | } 56 | 57 | message OnlineReq { 58 | string server = 1; 59 | map roomCount = 2; 60 | } 61 | 62 | message OnlineReply { 63 | map allRoomCount = 1; 64 | } 65 | 66 | message ReceiveReq { 67 | int64 mid = 1; 68 | goim.protocol.Proto proto = 2; 69 | } 70 | 71 | message ReceiveReply { 72 | } 73 | 74 | message NodesReq { 75 | string platform = 1; 76 | string clientIP = 2; 77 | } 78 | 79 | message NodesReply { 80 | string domain = 1; 81 | int32 tcp_port = 2; 82 | int32 ws_port = 3; 83 | int32 wss_port = 4; 84 | int32 heartbeat = 5; 85 | repeated string nodes = 6; 86 | Backoff backoff = 7; 87 | int32 heartbeat_max = 8; 88 | } 89 | 90 | message Backoff { 91 | int32 max_delay = 1; 92 | int32 base_delay = 2; 93 | float factor = 3; 94 | float jitter = 4; 95 | } 96 | 97 | service Logic { 98 | // Connect 99 | rpc Connect(ConnectReq) returns (ConnectReply); 100 | // Disconnect 101 | rpc Disconnect(DisconnectReq) returns (DisconnectReply); 102 | // Heartbeat 103 | rpc Heartbeat(HeartbeatReq) returns (HeartbeatReply); 104 | // RenewOnline 105 | rpc RenewOnline(OnlineReq) returns (OnlineReply); 106 | // Receive 107 | rpc Receive(ReceiveReq) returns (ReceiveReply); 108 | //ServerList 109 | rpc Nodes(NodesReq) returns (NodesReply); 110 | } 111 | -------------------------------------------------------------------------------- /api/protocol/operation.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | const ( 4 | // OpHandshake handshake 5 | OpHandshake = int32(0) 6 | // OpHandshakeReply handshake reply 7 | OpHandshakeReply = int32(1) 8 | 9 | // OpHeartbeat heartbeat 10 | OpHeartbeat = int32(2) 11 | // OpHeartbeatReply heartbeat reply 12 | OpHeartbeatReply = int32(3) 13 | 14 | // OpSendMsg send message. 15 | OpSendMsg = int32(4) 16 | // OpSendMsgReply send message reply 17 | OpSendMsgReply = int32(5) 18 | 19 | // OpDisconnectReply disconnect reply 20 | OpDisconnectReply = int32(6) 21 | 22 | // OpAuth auth connnect 23 | OpAuth = int32(7) 24 | // OpAuthReply auth connect reply 25 | OpAuthReply = int32(8) 26 | 27 | // OpRaw raw message 28 | OpRaw = int32(9) 29 | 30 | // OpProtoReady proto ready 31 | OpProtoReady = int32(10) 32 | // OpProtoFinish proto finish 33 | OpProtoFinish = int32(11) 34 | 35 | // OpChangeRoom change room 36 | OpChangeRoom = int32(12) 37 | // OpChangeRoomReply change room reply 38 | OpChangeRoomReply = int32(13) 39 | 40 | // OpSub subscribe operation 41 | OpSub = int32(14) 42 | // OpSubReply subscribe operation 43 | OpSubReply = int32(15) 44 | 45 | // OpUnsub unsubscribe operation 46 | OpUnsub = int32(16) 47 | // OpUnsubReply unsubscribe operation reply 48 | OpUnsubReply = int32(17) 49 | ) 50 | -------------------------------------------------------------------------------- /api/protocol/protocol.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: protocol/protocol.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | // 24 | // v1.0.0 25 | // protocol 26 | type Proto struct { 27 | Ver int32 `protobuf:"varint,1,opt,name=ver,proto3" json:"ver,omitempty"` 28 | Op int32 `protobuf:"varint,2,opt,name=op,proto3" json:"op,omitempty"` 29 | Seq int32 `protobuf:"varint,3,opt,name=seq,proto3" json:"seq,omitempty"` 30 | Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` 31 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 32 | XXX_unrecognized []byte `json:"-"` 33 | XXX_sizecache int32 `json:"-"` 34 | } 35 | 36 | func (m *Proto) Reset() { *m = Proto{} } 37 | func (m *Proto) String() string { return proto.CompactTextString(m) } 38 | func (*Proto) ProtoMessage() {} 39 | func (*Proto) Descriptor() ([]byte, []int) { 40 | return fileDescriptor_87968d26f3046c60, []int{0} 41 | } 42 | 43 | func (m *Proto) XXX_Unmarshal(b []byte) error { 44 | return xxx_messageInfo_Proto.Unmarshal(m, b) 45 | } 46 | func (m *Proto) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 47 | return xxx_messageInfo_Proto.Marshal(b, m, deterministic) 48 | } 49 | func (m *Proto) XXX_Merge(src proto.Message) { 50 | xxx_messageInfo_Proto.Merge(m, src) 51 | } 52 | func (m *Proto) XXX_Size() int { 53 | return xxx_messageInfo_Proto.Size(m) 54 | } 55 | func (m *Proto) XXX_DiscardUnknown() { 56 | xxx_messageInfo_Proto.DiscardUnknown(m) 57 | } 58 | 59 | var xxx_messageInfo_Proto proto.InternalMessageInfo 60 | 61 | func (m *Proto) GetVer() int32 { 62 | if m != nil { 63 | return m.Ver 64 | } 65 | return 0 66 | } 67 | 68 | func (m *Proto) GetOp() int32 { 69 | if m != nil { 70 | return m.Op 71 | } 72 | return 0 73 | } 74 | 75 | func (m *Proto) GetSeq() int32 { 76 | if m != nil { 77 | return m.Seq 78 | } 79 | return 0 80 | } 81 | 82 | func (m *Proto) GetBody() []byte { 83 | if m != nil { 84 | return m.Body 85 | } 86 | return nil 87 | } 88 | 89 | func init() { 90 | proto.RegisterType((*Proto)(nil), "goim.protocol.Proto") 91 | } 92 | 93 | func init() { proto.RegisterFile("protocol/protocol.proto", fileDescriptor_87968d26f3046c60) } 94 | 95 | var fileDescriptor_87968d26f3046c60 = []byte{ 96 | // 153 bytes of a gzipped FileDescriptorProto 97 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2f, 0x28, 0xca, 0x2f, 98 | 0xc9, 0x4f, 0xce, 0xcf, 0xd1, 0x87, 0x31, 0xf4, 0xc0, 0x0c, 0x21, 0xde, 0xf4, 0xfc, 0xcc, 0x5c, 99 | 0x3d, 0x98, 0xa0, 0x92, 0x3f, 0x17, 0x6b, 0x00, 0x58, 0x5c, 0x80, 0x8b, 0xb9, 0x2c, 0xb5, 0x48, 100 | 0x82, 0x51, 0x81, 0x51, 0x83, 0x35, 0x08, 0xc4, 0x14, 0xe2, 0xe3, 0x62, 0xca, 0x2f, 0x90, 0x60, 101 | 0x02, 0x0b, 0x30, 0xe5, 0x17, 0x80, 0x54, 0x14, 0xa7, 0x16, 0x4a, 0x30, 0x43, 0x54, 0x14, 0xa7, 102 | 0x16, 0x0a, 0x09, 0x71, 0xb1, 0x24, 0xe5, 0xa7, 0x54, 0x4a, 0xb0, 0x28, 0x30, 0x6a, 0xf0, 0x04, 103 | 0x81, 0xd9, 0x4e, 0x86, 0x51, 0xfa, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 104 | 0xfa, 0x21, 0xa9, 0x45, 0x45, 0x95, 0xba, 0xbe, 0x89, 0xf9, 0xfa, 0x20, 0x6b, 0xf5, 0x13, 0x0b, 105 | 0x32, 0xe1, 0xee, 0xb1, 0x86, 0x31, 0x92, 0xd8, 0xc0, 0x2c, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 106 | 0xff, 0x8d, 0xc7, 0xbf, 0x64, 0xb4, 0x00, 0x00, 0x00, 107 | } 108 | -------------------------------------------------------------------------------- /api/protocol/protocol.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package goim.protocol; 4 | 5 | option go_package = "github.com/Terry-Mao/goim/api/protocol;protocol"; 6 | 7 | /* 8 | * v1.0.0 9 | * protocol 10 | */ 11 | message Proto { 12 | int32 ver = 1; 13 | int32 op = 2; 14 | int32 seq = 3; 15 | bytes body = 4; 16 | } 17 | -------------------------------------------------------------------------------- /benchmarks/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Start Commond eg: ./client 1 1000 localhost:3101 4 | // first parameter:beginning userId 5 | // second parameter: amount of clients 6 | // third parameter: comet server ip 7 | 8 | import ( 9 | "bufio" 10 | "encoding/binary" 11 | "encoding/json" 12 | "flag" 13 | "fmt" 14 | "math/rand" 15 | "net" 16 | "os" 17 | "runtime" 18 | "strconv" 19 | "sync/atomic" 20 | "time" 21 | 22 | log "github.com/golang/glog" 23 | ) 24 | 25 | const ( 26 | opHeartbeat = int32(2) 27 | opHeartbeatReply = int32(3) 28 | opAuth = int32(7) 29 | opAuthReply = int32(8) 30 | ) 31 | 32 | const ( 33 | rawHeaderLen = uint16(16) 34 | heart = 240 * time.Second 35 | ) 36 | 37 | // Proto proto. 38 | type Proto struct { 39 | PackLen int32 // package length 40 | HeaderLen int16 // header length 41 | Ver int16 // protocol version 42 | Operation int32 // operation for request 43 | Seq int32 // sequence number chosen by client 44 | Body []byte // body 45 | } 46 | 47 | // AuthToken auth token. 48 | type AuthToken struct { 49 | Mid int64 `json:"mid"` 50 | Key string `json:"key"` 51 | RoomID string `json:"room_id"` 52 | Platform string `json:"platform"` 53 | Accepts []int32 `json:"accepts"` 54 | } 55 | 56 | var ( 57 | countDown int64 58 | aliveCount int64 59 | ) 60 | 61 | func main() { 62 | runtime.GOMAXPROCS(runtime.NumCPU()) 63 | flag.Parse() 64 | begin, err := strconv.Atoi(os.Args[1]) 65 | if err != nil { 66 | panic(err) 67 | } 68 | num, err := strconv.Atoi(os.Args[2]) 69 | if err != nil { 70 | panic(err) 71 | } 72 | go result() 73 | for i := begin; i < begin+num; i++ { 74 | go client(int64(i)) 75 | } 76 | // signal 77 | var exit chan bool 78 | <-exit 79 | } 80 | 81 | func result() { 82 | var ( 83 | lastTimes int64 84 | interval = int64(5) 85 | ) 86 | for { 87 | nowCount := atomic.LoadInt64(&countDown) 88 | nowAlive := atomic.LoadInt64(&aliveCount) 89 | diff := nowCount - lastTimes 90 | lastTimes = nowCount 91 | fmt.Println(fmt.Sprintf("%s alive:%d down:%d down/s:%d", time.Now().Format("2006-01-02 15:04:05"), nowAlive, nowCount, diff/interval)) 92 | time.Sleep(time.Second * time.Duration(interval)) 93 | } 94 | } 95 | 96 | func client(mid int64) { 97 | for { 98 | startClient(mid) 99 | time.Sleep(time.Duration(rand.Intn(10)) * time.Second) 100 | } 101 | } 102 | 103 | func startClient(key int64) { 104 | time.Sleep(time.Duration(rand.Intn(120)) * time.Second) 105 | atomic.AddInt64(&aliveCount, 1) 106 | quit := make(chan bool, 1) 107 | defer func() { 108 | close(quit) 109 | atomic.AddInt64(&aliveCount, -1) 110 | }() 111 | // connnect to server 112 | conn, err := net.Dial("tcp", os.Args[3]) 113 | if err != nil { 114 | log.Errorf("net.Dial(%s) error(%v)", os.Args[3], err) 115 | return 116 | } 117 | seq := int32(0) 118 | wr := bufio.NewWriter(conn) 119 | rd := bufio.NewReader(conn) 120 | authToken := &AuthToken{ 121 | key, 122 | "", 123 | "test://1", 124 | "ios", 125 | []int32{1000, 1001, 1002}, 126 | } 127 | proto := new(Proto) 128 | proto.Ver = 1 129 | proto.Operation = opAuth 130 | proto.Seq = seq 131 | proto.Body, _ = json.Marshal(authToken) 132 | if err = tcpWriteProto(wr, proto); err != nil { 133 | log.Errorf("tcpWriteProto() error(%v)", err) 134 | return 135 | } 136 | if err = tcpReadProto(rd, proto); err != nil { 137 | log.Errorf("tcpReadProto() error(%v)", err) 138 | return 139 | } 140 | log.Infof("key:%d auth ok, proto: %v", key, proto) 141 | seq++ 142 | // writer 143 | go func() { 144 | hbProto := new(Proto) 145 | for { 146 | // heartbeat 147 | hbProto.Operation = opHeartbeat 148 | hbProto.Seq = seq 149 | hbProto.Body = nil 150 | if err = tcpWriteProto(wr, hbProto); err != nil { 151 | log.Errorf("key:%d tcpWriteProto() error(%v)", key, err) 152 | return 153 | } 154 | log.Infof("key:%d Write heartbeat", key) 155 | time.Sleep(heart) 156 | seq++ 157 | select { 158 | case <-quit: 159 | return 160 | default: 161 | } 162 | } 163 | }() 164 | // reader 165 | for { 166 | if err = tcpReadProto(rd, proto); err != nil { 167 | log.Errorf("key:%d tcpReadProto() error(%v)", key, err) 168 | quit <- true 169 | return 170 | } 171 | if proto.Operation == opAuthReply { 172 | log.Infof("key:%d auth success", key) 173 | } else if proto.Operation == opHeartbeatReply { 174 | log.Infof("key:%d receive heartbeat", key) 175 | if err = conn.SetReadDeadline(time.Now().Add(heart + 60*time.Second)); err != nil { 176 | log.Errorf("conn.SetReadDeadline() error(%v)", err) 177 | quit <- true 178 | return 179 | } 180 | } else { 181 | log.Infof("key:%d op:%d msg: %s", key, proto.Operation, string(proto.Body)) 182 | atomic.AddInt64(&countDown, 1) 183 | } 184 | } 185 | } 186 | 187 | func tcpWriteProto(wr *bufio.Writer, proto *Proto) (err error) { 188 | // write 189 | if err = binary.Write(wr, binary.BigEndian, uint32(rawHeaderLen)+uint32(len(proto.Body))); err != nil { 190 | return 191 | } 192 | if err = binary.Write(wr, binary.BigEndian, rawHeaderLen); err != nil { 193 | return 194 | } 195 | if err = binary.Write(wr, binary.BigEndian, proto.Ver); err != nil { 196 | return 197 | } 198 | if err = binary.Write(wr, binary.BigEndian, proto.Operation); err != nil { 199 | return 200 | } 201 | if err = binary.Write(wr, binary.BigEndian, proto.Seq); err != nil { 202 | return 203 | } 204 | if proto.Body != nil { 205 | if err = binary.Write(wr, binary.BigEndian, proto.Body); err != nil { 206 | return 207 | } 208 | } 209 | err = wr.Flush() 210 | return 211 | } 212 | 213 | func tcpReadProto(rd *bufio.Reader, proto *Proto) (err error) { 214 | var ( 215 | packLen int32 216 | headerLen int16 217 | ) 218 | // read 219 | if err = binary.Read(rd, binary.BigEndian, &packLen); err != nil { 220 | return 221 | } 222 | if err = binary.Read(rd, binary.BigEndian, &headerLen); err != nil { 223 | return 224 | } 225 | if err = binary.Read(rd, binary.BigEndian, &proto.Ver); err != nil { 226 | return 227 | } 228 | if err = binary.Read(rd, binary.BigEndian, &proto.Operation); err != nil { 229 | return 230 | } 231 | if err = binary.Read(rd, binary.BigEndian, &proto.Seq); err != nil { 232 | return 233 | } 234 | var ( 235 | n, t int 236 | bodyLen = int(packLen - int32(headerLen)) 237 | ) 238 | if bodyLen > 0 { 239 | proto.Body = make([]byte, bodyLen) 240 | for { 241 | if t, err = rd.Read(proto.Body[n:]); err != nil { 242 | return 243 | } 244 | if n += t; n == bodyLen { 245 | break 246 | } 247 | } 248 | } else { 249 | proto.Body = nil 250 | } 251 | return 252 | } 253 | -------------------------------------------------------------------------------- /benchmarks/multi_push/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Start Command eg : ./multi_push 0 20000 localhost:7172 60 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "runtime" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | var ( 21 | lg *log.Logger 22 | httpClient *http.Client 23 | t int 24 | ) 25 | 26 | const testContent = "{\"test\":1}" 27 | 28 | type pushsBodyMsg struct { 29 | Msg json.RawMessage `json:"m"` 30 | UserIds []int64 `json:"u"` 31 | } 32 | 33 | func init() { 34 | httpTransport := &http.Transport{ 35 | Dial: func(netw, addr string) (net.Conn, error) { 36 | deadline := time.Now().Add(30 * time.Second) 37 | c, err := net.DialTimeout(netw, addr, 20*time.Second) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | _ = c.SetDeadline(deadline) 43 | return c, nil 44 | }, 45 | DisableKeepAlives: false, 46 | } 47 | httpClient = &http.Client{ 48 | Transport: httpTransport, 49 | } 50 | } 51 | 52 | func main() { 53 | runtime.GOMAXPROCS(runtime.NumCPU()) 54 | infoLogfi, err := os.OpenFile("./multi_push.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 55 | if err != nil { 56 | panic(err) 57 | } 58 | lg = log.New(infoLogfi, "", log.LstdFlags|log.Lshortfile) 59 | 60 | begin, err := strconv.Atoi(os.Args[1]) 61 | if err != nil { 62 | panic(err) 63 | } 64 | length, err := strconv.Atoi(os.Args[2]) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | t, err = strconv.Atoi(os.Args[4]) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | num := runtime.NumCPU() * 8 75 | 76 | l := length / num 77 | b, e := begin, begin+l 78 | time.AfterFunc(time.Duration(t)*time.Second, stop) 79 | for i := 0; i < num; i++ { 80 | go startPush(b, e) 81 | b += l 82 | e += l 83 | } 84 | if b < begin+length { 85 | go startPush(b, begin+length) 86 | } 87 | 88 | time.Sleep(9999 * time.Hour) 89 | } 90 | 91 | func stop() { 92 | os.Exit(-1) 93 | } 94 | 95 | func startPush(b, e int) { 96 | l := make([]int64, 0, e-b) 97 | for i := b; i < e; i++ { 98 | l = append(l, int64(i)) 99 | } 100 | msg := &pushsBodyMsg{Msg: json.RawMessage(testContent), UserIds: l} 101 | body, err := json.Marshal(msg) 102 | if err != nil { 103 | panic(err) 104 | } 105 | for { 106 | resp, err := httpPost(fmt.Sprintf("http://%s/goim/push/mids=%d", os.Args[3], b), "application/x-www-form-urlencoded", bytes.NewBuffer(body)) 107 | if err != nil { 108 | lg.Printf("post error (%v)", err) 109 | continue 110 | } 111 | 112 | body, err := ioutil.ReadAll(resp.Body) 113 | if err != nil { 114 | lg.Printf("post error (%v)", err) 115 | return 116 | } 117 | resp.Body.Close() 118 | 119 | lg.Printf("response %s", string(body)) 120 | } 121 | } 122 | 123 | func httpPost(url string, contentType string, body io.Reader) (*http.Response, error) { 124 | req, err := http.NewRequest("POST", url, body) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | req.Header.Set("Content-Type", contentType) 130 | resp, err := httpClient.Do(req) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | return resp, nil 136 | } 137 | -------------------------------------------------------------------------------- /benchmarks/push/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Start Command eg : ./push 0 20000 localhost:7172 60 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "runtime" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | var ( 21 | httpClient *http.Client 22 | t int 23 | ) 24 | 25 | const testContent = "{\"test\":1}" 26 | 27 | type pushBodyMsg struct { 28 | Msg json.RawMessage `json:"m"` 29 | UserID int64 `json:"u"` 30 | } 31 | 32 | func init() { 33 | httpTransport := &http.Transport{ 34 | Dial: func(netw, addr string) (net.Conn, error) { 35 | deadline := time.Now().Add(30 * time.Second) 36 | c, err := net.DialTimeout(netw, addr, 20*time.Second) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | _ = c.SetDeadline(deadline) 42 | return c, nil 43 | }, 44 | DisableKeepAlives: false, 45 | } 46 | httpClient = &http.Client{ 47 | Transport: httpTransport, 48 | } 49 | } 50 | 51 | func main() { 52 | runtime.GOMAXPROCS(runtime.NumCPU()) 53 | begin, err := strconv.Atoi(os.Args[1]) 54 | if err != nil { 55 | panic(err) 56 | } 57 | length, err := strconv.Atoi(os.Args[2]) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | t, err = strconv.Atoi(os.Args[4]) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | num := runtime.NumCPU() * 2 68 | log.Printf("start routine num:%d", num) 69 | 70 | l := length / num 71 | b, e := begin, begin+l 72 | time.AfterFunc(time.Duration(t)*time.Second, stop) 73 | for i := 0; i < num; i++ { 74 | go startPush(b, e) 75 | b += l 76 | e += l 77 | } 78 | if b < begin+length { 79 | go startPush(b, begin+length) 80 | } 81 | 82 | time.Sleep(9999 * time.Hour) 83 | } 84 | 85 | func stop() { 86 | os.Exit(-1) 87 | } 88 | 89 | func startPush(b, e int) { 90 | log.Printf("start Push from %d to %d", b, e) 91 | bodys := make([][]byte, e-b) 92 | for i := 0; i < e-b; i++ { 93 | msg := &pushBodyMsg{Msg: json.RawMessage(testContent), UserID: int64(b)} 94 | body, err := json.Marshal(msg) 95 | if err != nil { 96 | panic(err) 97 | } 98 | bodys[i] = body 99 | } 100 | 101 | for { 102 | for i := 0; i < len(bodys); i++ { 103 | resp, err := httpPost(fmt.Sprintf("http://%s/goim/push/mids?operation=1000&mids=%d", os.Args[3], b), "application/x-www-form-urlencoded", bytes.NewBuffer(bodys[i])) 104 | if err != nil { 105 | log.Printf("post error (%v)", err) 106 | continue 107 | } 108 | 109 | body, err := ioutil.ReadAll(resp.Body) 110 | if err != nil { 111 | log.Printf("post error (%v)", err) 112 | return 113 | } 114 | resp.Body.Close() 115 | 116 | log.Printf("response %s", string(body)) 117 | //time.Sleep(50 * time.Millisecond) 118 | } 119 | } 120 | } 121 | 122 | func httpPost(url string, contentType string, body io.Reader) (*http.Response, error) { 123 | req, err := http.NewRequest("POST", url, body) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | req.Header.Set("Content-Type", contentType) 129 | resp, err := httpClient.Do(req) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return resp, nil 135 | } 136 | -------------------------------------------------------------------------------- /benchmarks/push_room/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Start Commond eg: ./push_room 1 20 localhost:3111 4 | // first parameter: room id 5 | // second parameter: num per seconds 6 | // third parameter: logic server ip 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | func main() { 19 | rountineNum, err := strconv.Atoi(os.Args[2]) 20 | if err != nil { 21 | panic(err) 22 | } 23 | addr := os.Args[3] 24 | 25 | gap := time.Second / time.Duration(rountineNum) 26 | delay := time.Duration(0) 27 | 28 | go run(addr, time.Duration(0)*time.Second) 29 | for i := 0; i < rountineNum-1; i++ { 30 | go run(addr, delay) 31 | delay += gap 32 | fmt.Println("delay:", delay) 33 | } 34 | time.Sleep(9999 * time.Hour) 35 | } 36 | 37 | func run(addr string, delay time.Duration) { 38 | time.Sleep(delay) 39 | i := int64(0) 40 | for { 41 | go post(addr, i) 42 | time.Sleep(time.Second) 43 | i++ 44 | } 45 | } 46 | 47 | func post(addr string, i int64) { 48 | resp, err := http.Post("http://"+addr+"/goim/push/room?operation=1000&type=test&room="+os.Args[1], "application/json", bytes.NewBufferString(fmt.Sprintf("{\"test\":%d}", i))) 49 | if err != nil { 50 | fmt.Printf("Error: http.post() error(%v)\n", err) 51 | return 52 | } 53 | defer resp.Body.Close() 54 | body, err := ioutil.ReadAll(resp.Body) 55 | if err != nil { 56 | fmt.Printf("Error: http.post() error(%v)\n", err) 57 | return 58 | } 59 | 60 | fmt.Printf("%s postId:%d, response:%s\n", time.Now().Format("2006-01-02 15:04:05"), i, string(body)) 61 | } 62 | -------------------------------------------------------------------------------- /benchmarks/push_rooms/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Start Command eg : ./push_rooms 0 20000 localhost:7172 40 4 | // param 1 : the start of room number 5 | // param 2 : the end of room number 6 | // param 3 : comet server tcp address 7 | // param 4 : push amount each goroutines per second 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "net" 15 | "net/http" 16 | "os" 17 | "runtime" 18 | "strconv" 19 | "time" 20 | ) 21 | 22 | var ( 23 | httpClient *http.Client 24 | ) 25 | 26 | const testContent = "{\"test\":1}" 27 | 28 | func init() { 29 | httpTransport := &http.Transport{ 30 | Dial: func(netw, addr string) (net.Conn, error) { 31 | deadline := time.Now().Add(30 * time.Second) 32 | c, err := net.DialTimeout(netw, addr, 20*time.Second) 33 | if err != nil { 34 | return nil, err 35 | } 36 | _ = c.SetDeadline(deadline) 37 | return c, nil 38 | }, 39 | DisableKeepAlives: false, 40 | } 41 | httpClient = &http.Client{ 42 | Transport: httpTransport, 43 | } 44 | } 45 | 46 | func main() { 47 | runtime.GOMAXPROCS(runtime.NumCPU()) 48 | begin, err := strconv.Atoi(os.Args[1]) 49 | if err != nil { 50 | panic(err) 51 | } 52 | length, err := strconv.Atoi(os.Args[2]) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | num, err := strconv.Atoi(os.Args[4]) 58 | if err != nil { 59 | panic(err) 60 | } 61 | delay := (1000 * time.Millisecond) / time.Duration(num) 62 | 63 | routines := runtime.NumCPU() * 2 64 | log.Printf("start routine num:%d", routines) 65 | 66 | l := length / routines 67 | b, e := begin, begin+l 68 | for i := 0; i < routines; i++ { 69 | go startPush(b, e, delay) 70 | b += l 71 | e += l 72 | } 73 | if b < begin+length { 74 | go startPush(b, begin+length, delay) 75 | } 76 | 77 | time.Sleep(9999 * time.Hour) 78 | } 79 | 80 | func startPush(b, e int, delay time.Duration) { 81 | log.Printf("start Push from %d to %d", b, e) 82 | 83 | for { 84 | for i := b; i < e; i++ { 85 | resp, err := http.Post(fmt.Sprintf("http://%s/goim/push/room?operation=1000&type=test&room=%d", os.Args[3], i), "application/json", bytes.NewBufferString(testContent)) 86 | if err != nil { 87 | log.Printf("post error (%v)", err) 88 | continue 89 | } 90 | 91 | body, err := ioutil.ReadAll(resp.Body) 92 | if err != nil { 93 | log.Printf("post error (%v)", err) 94 | return 95 | } 96 | resp.Body.Close() 97 | 98 | log.Printf("push room:%d response %s", i, string(body)) 99 | time.Sleep(delay) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cmd/comet/comet-example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom 2 | [discovery] 3 | nodes = ["127.0.0.1:7171"] 4 | 5 | [rpcServer] 6 | addr = ":3109" 7 | timeout = "1s" 8 | 9 | [rpcClient] 10 | dial = "1s" 11 | timeout = "1s" 12 | 13 | [tcp] 14 | bind = [":3101"] 15 | sndbuf = 4096 16 | rcvbuf = 4096 17 | keepalive = false 18 | reader = 32 19 | readBuf = 1024 20 | readBufSize = 8192 21 | writer = 32 22 | writeBuf = 1024 23 | writeBufSize = 8192 24 | 25 | [websocket] 26 | bind = [":3102"] 27 | tlsOpen = false 28 | tlsBind = [":3103"] 29 | certFile = "../../cert.pem" 30 | privateFile = "../../private.pem" 31 | 32 | [protocol] 33 | timer = 32 34 | timerSize = 2048 35 | svrProto = 10 36 | cliProto = 5 37 | handshakeTimeout = "8s" 38 | 39 | [whitelist] 40 | Whitelist = [123] 41 | WhiteLog = "/tmp/white_list.log" 42 | 43 | [bucket] 44 | size = 32 45 | channel = 1024 46 | room = 1024 47 | routineAmount = 32 48 | routineSize = 1024 49 | -------------------------------------------------------------------------------- /cmd/comet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "math/rand" 8 | "net" 9 | "os" 10 | "os/signal" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/bilibili/discovery/naming" 18 | resolver "github.com/bilibili/discovery/naming/grpc" 19 | "github.com/Terry-Mao/goim/internal/comet" 20 | "github.com/Terry-Mao/goim/internal/comet/conf" 21 | "github.com/Terry-Mao/goim/internal/comet/grpc" 22 | md "github.com/Terry-Mao/goim/internal/logic/model" 23 | "github.com/Terry-Mao/goim/pkg/ip" 24 | log "github.com/golang/glog" 25 | ) 26 | 27 | const ( 28 | ver = "2.0.0" 29 | appid = "goim.comet" 30 | ) 31 | 32 | func main() { 33 | flag.Parse() 34 | if err := conf.Init(); err != nil { 35 | panic(err) 36 | } 37 | rand.Seed(time.Now().UTC().UnixNano()) 38 | runtime.GOMAXPROCS(runtime.NumCPU()) 39 | println(conf.Conf.Debug) 40 | log.Infof("goim-comet [version: %s env: %+v] start", ver, conf.Conf.Env) 41 | // register discovery 42 | dis := naming.New(conf.Conf.Discovery) 43 | resolver.Register(dis) 44 | // new comet server 45 | srv := comet.NewServer(conf.Conf) 46 | if err := comet.InitWhitelist(conf.Conf.Whitelist); err != nil { 47 | panic(err) 48 | } 49 | if err := comet.InitTCP(srv, conf.Conf.TCP.Bind, runtime.NumCPU()); err != nil { 50 | panic(err) 51 | } 52 | if err := comet.InitWebsocket(srv, conf.Conf.Websocket.Bind, runtime.NumCPU()); err != nil { 53 | panic(err) 54 | } 55 | if conf.Conf.Websocket.TLSOpen { 56 | if err := comet.InitWebsocketWithTLS(srv, conf.Conf.Websocket.TLSBind, conf.Conf.Websocket.CertFile, conf.Conf.Websocket.PrivateFile, runtime.NumCPU()); err != nil { 57 | panic(err) 58 | } 59 | } 60 | // new grpc server 61 | rpcSrv := grpc.New(conf.Conf.RPCServer, srv) 62 | cancel := register(dis, srv) 63 | // signal 64 | c := make(chan os.Signal, 1) 65 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) 66 | for { 67 | s := <-c 68 | log.Infof("goim-comet get a signal %s", s.String()) 69 | switch s { 70 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: 71 | if cancel != nil { 72 | cancel() 73 | } 74 | rpcSrv.GracefulStop() 75 | srv.Close() 76 | log.Infof("goim-comet [version: %s] exit", ver) 77 | log.Flush() 78 | return 79 | case syscall.SIGHUP: 80 | default: 81 | return 82 | } 83 | } 84 | } 85 | 86 | func register(dis *naming.Discovery, srv *comet.Server) context.CancelFunc { 87 | env := conf.Conf.Env 88 | addr := ip.InternalIP() 89 | _, port, _ := net.SplitHostPort(conf.Conf.RPCServer.Addr) 90 | ins := &naming.Instance{ 91 | Region: env.Region, 92 | Zone: env.Zone, 93 | Env: env.DeployEnv, 94 | Hostname: env.Host, 95 | AppID: appid, 96 | Addrs: []string{ 97 | "grpc://" + addr + ":" + port, 98 | }, 99 | Metadata: map[string]string{ 100 | md.MetaWeight: strconv.FormatInt(env.Weight, 10), 101 | md.MetaOffline: strconv.FormatBool(env.Offline), 102 | md.MetaAddrs: strings.Join(env.Addrs, ","), 103 | }, 104 | } 105 | cancel, err := dis.Register(ins) 106 | if err != nil { 107 | panic(err) 108 | } 109 | // renew discovery metadata 110 | go func() { 111 | for { 112 | var ( 113 | err error 114 | conns int 115 | ips = make(map[string]struct{}) 116 | ) 117 | for _, bucket := range srv.Buckets() { 118 | for ip := range bucket.IPCount() { 119 | ips[ip] = struct{}{} 120 | } 121 | conns += bucket.ChannelCount() 122 | } 123 | ins.Metadata[md.MetaConnCount] = fmt.Sprint(conns) 124 | ins.Metadata[md.MetaIPCount] = fmt.Sprint(len(ips)) 125 | if err = dis.Set(ins); err != nil { 126 | log.Errorf("dis.Set(%+v) error(%v)", ins, err) 127 | time.Sleep(time.Second) 128 | continue 129 | } 130 | time.Sleep(time.Second * 10) 131 | } 132 | }() 133 | return cancel 134 | } 135 | -------------------------------------------------------------------------------- /cmd/job/job-example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom 2 | [discovery] 3 | nodes = ["127.0.0.1:7171"] 4 | 5 | [kafka] 6 | topic = "goim-push-topic" 7 | group = "goim-push-group-job" 8 | brokers = ["127.0.0.1:9092"] 9 | -------------------------------------------------------------------------------- /cmd/job/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/bilibili/discovery/naming" 10 | "github.com/Terry-Mao/goim/internal/job" 11 | "github.com/Terry-Mao/goim/internal/job/conf" 12 | 13 | resolver "github.com/bilibili/discovery/naming/grpc" 14 | log "github.com/golang/glog" 15 | ) 16 | 17 | var ( 18 | ver = "2.0.0" 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | if err := conf.Init(); err != nil { 24 | panic(err) 25 | } 26 | log.Infof("goim-job [version: %s env: %+v] start", ver, conf.Conf.Env) 27 | // grpc register naming 28 | dis := naming.New(conf.Conf.Discovery) 29 | resolver.Register(dis) 30 | // job 31 | j := job.New(conf.Conf) 32 | go j.Consume() 33 | // signal 34 | c := make(chan os.Signal, 1) 35 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) 36 | for { 37 | s := <-c 38 | log.Infof("goim-job get a signal %s", s.String()) 39 | switch s { 40 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: 41 | j.Close() 42 | log.Infof("goim-job [version: %s] exit", ver) 43 | log.Flush() 44 | return 45 | case syscall.SIGHUP: 46 | default: 47 | return 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/logic/logic-example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom 2 | [discovery] 3 | nodes = ["127.0.0.1:7171"] 4 | 5 | [regions] 6 | "bj" = ["北京","天津","河北","山东","山西","内蒙古","辽宁","吉林","黑龙江","甘肃","宁夏","新疆"] 7 | "sh" = ["上海","江苏","浙江","安徽","江西","湖北","重庆","陕西","青海","河南","台湾"] 8 | "gz" = ["广东","福建","广西","海南","湖南","四川","贵州","云南","西藏","香港","澳门"] 9 | 10 | [node] 11 | defaultDomain = "conn.goim.io" 12 | hostDomain = ".goim.io" 13 | heartbeat = "4m" 14 | heartbeatMax = 2 15 | tcpPort = 3101 16 | wsPort = 3102 17 | wssPort = 3103 18 | regionWeight = 1.6 19 | 20 | [backoff] 21 | maxDelay = 300 22 | baseDelay = 3 23 | factor = 1.8 24 | jitter = 0.3 25 | 26 | [rpcServer] 27 | network = "tcp" 28 | addr = ":3119" 29 | timeout = "1s" 30 | 31 | [rpcClient] 32 | dial = "1s" 33 | timeout = "1s" 34 | 35 | [httpServer] 36 | network = "tcp" 37 | addr = ":3111" 38 | readTimeout = "1s" 39 | writeTimeout = "1s" 40 | 41 | [kafka] 42 | topic = "goim-push-topic" 43 | brokers = ["127.0.0.1:9092"] 44 | 45 | [redis] 46 | network = "tcp" 47 | addr = "127.0.0.1:6379" 48 | active = 60000 49 | idle = 1024 50 | dialTimeout = "200ms" 51 | readTimeout = "500ms" 52 | writeTimeout = "500ms" 53 | idleTimeout = "120s" 54 | expire = "30m" 55 | -------------------------------------------------------------------------------- /cmd/logic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "strconv" 10 | "syscall" 11 | 12 | "github.com/bilibili/discovery/naming" 13 | resolver "github.com/bilibili/discovery/naming/grpc" 14 | "github.com/Terry-Mao/goim/internal/logic" 15 | "github.com/Terry-Mao/goim/internal/logic/conf" 16 | "github.com/Terry-Mao/goim/internal/logic/grpc" 17 | "github.com/Terry-Mao/goim/internal/logic/http" 18 | "github.com/Terry-Mao/goim/internal/logic/model" 19 | "github.com/Terry-Mao/goim/pkg/ip" 20 | log "github.com/golang/glog" 21 | ) 22 | 23 | const ( 24 | ver = "2.0.0" 25 | appid = "goim.logic" 26 | ) 27 | 28 | func main() { 29 | flag.Parse() 30 | if err := conf.Init(); err != nil { 31 | panic(err) 32 | } 33 | log.Infof("goim-logic [version: %s env: %+v] start", ver, conf.Conf.Env) 34 | // grpc register naming 35 | dis := naming.New(conf.Conf.Discovery) 36 | resolver.Register(dis) 37 | // logic 38 | srv := logic.New(conf.Conf) 39 | httpSrv := http.New(conf.Conf.HTTPServer, srv) 40 | rpcSrv := grpc.New(conf.Conf.RPCServer, srv) 41 | cancel := register(dis, srv) 42 | // signal 43 | c := make(chan os.Signal, 1) 44 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) 45 | for { 46 | s := <-c 47 | log.Infof("goim-logic get a signal %s", s.String()) 48 | switch s { 49 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: 50 | if cancel != nil { 51 | cancel() 52 | } 53 | srv.Close() 54 | httpSrv.Close() 55 | rpcSrv.GracefulStop() 56 | log.Infof("goim-logic [version: %s] exit", ver) 57 | log.Flush() 58 | return 59 | case syscall.SIGHUP: 60 | default: 61 | return 62 | } 63 | } 64 | } 65 | 66 | func register(dis *naming.Discovery, srv *logic.Logic) context.CancelFunc { 67 | env := conf.Conf.Env 68 | addr := ip.InternalIP() 69 | _, port, _ := net.SplitHostPort(conf.Conf.RPCServer.Addr) 70 | ins := &naming.Instance{ 71 | Region: env.Region, 72 | Zone: env.Zone, 73 | Env: env.DeployEnv, 74 | Hostname: env.Host, 75 | AppID: appid, 76 | Addrs: []string{ 77 | "grpc://" + addr + ":" + port, 78 | }, 79 | Metadata: map[string]string{ 80 | model.MetaWeight: strconv.FormatInt(env.Weight, 10), 81 | }, 82 | } 83 | cancel, err := dis.Register(ins) 84 | if err != nil { 85 | panic(err) 86 | } 87 | return cancel 88 | } 89 | -------------------------------------------------------------------------------- /codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v cmd | grep -v docs | grep -v srcipts | grep -v benchmarks | grep -v examples); do 7 | echo "testing for $d ..." 8 | go test -coverprofile=profile.out -covermode=atomic $d 9 | if [ -f profile.out ]; then 10 | cat profile.out >> coverage.txt 11 | rm profile.out 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /docs/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/arch.png -------------------------------------------------------------------------------- /docs/benchmark-comet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/benchmark-comet.jpg -------------------------------------------------------------------------------- /docs/benchmark-flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/benchmark-flow.jpg -------------------------------------------------------------------------------- /docs/benchmark-heap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/benchmark-heap.jpg -------------------------------------------------------------------------------- /docs/benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/benchmark.jpg -------------------------------------------------------------------------------- /docs/benchmark_cn.md: -------------------------------------------------------------------------------- 1 | ## 压测图表 2 | ![benchmark](benchmark.jpg) 3 | 4 | ### 服务端配置 5 | | CPU | 内存 | 操作系统 | 数量 | 6 | | :---- | :---- | :---- | :---- | 7 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 | 8 | 9 | ### 压测参数 10 | * 不同UID同房间在线人数: 1,000,000 11 | * 持续推送时长: 15分钟 12 | * 持续推送数量: 40条/秒 13 | * 推送内容: {"test":1} 14 | * 推送类型: 单房间推送 15 | * 到达计算方式: 1秒统计一次,共30次 16 | 17 | ### 资源使用 18 | * 每台服务端CPU使用: 2000%~2300%(刚好满负载) 19 | * 每台服务端内存使用: 14GB左右 20 | * GC耗时: 504毫秒左右 21 | * 流量使用: Incoming(450MBit/s), Outgoing(4.39GBit/s) 22 | 23 | ### 压测结果 24 | * 推送到达: 3590万/秒左右; 25 | 26 | ## comet模块 27 | ![benchmark-comet](benchmark-comet.jpg) 28 | 29 | ## 流量 30 | ![benchmark-flow](benchmark-flow.jpg) 31 | 32 | ## heap信息(包含GC) 33 | ![benchmark-flow](benchmark-heap.jpg) 34 | -------------------------------------------------------------------------------- /docs/benchmark_en.md: -------------------------------------------------------------------------------- 1 | ## Benchmark Chart 2 | ![benchmark](benchmark.jpg) 3 | 4 | ### Benchmark Server 5 | | CPU | Memory | OS | Instance | 6 | | :---- | :---- | :---- | :---- | 7 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 | 8 | 9 | ### Benchmark Case 10 | * Online: 1,000,000 11 | * Duration: 15min 12 | * Push Speed: 40/s (broadcast room) 13 | * Push Message: {"test":1} 14 | * Received calc mode: 1s per times, total 30 times 15 | 16 | ### Benchmark Resource 17 | 18 | * CPU: 2000%~2300% 19 | * Memory: 14GB 20 | * GC Pause: 504ms 21 | * Network: Incoming(450MBit/s), Outgoing(4.39GBit/s) 22 | 23 | ### Benchmark Result 24 | * Received: 35,900,000/s 25 | 26 | ## Comet 27 | ![benchmark-comet](benchmark-comet.jpg) 28 | 29 | ## Network traffic 30 | ![benchmark-flow](benchmark-flow.jpg) 31 | 32 | ## Heap (include GC) 33 | ![benchmark-flow](benchmark-heap.jpg) 34 | -------------------------------------------------------------------------------- /docs/en/proto.md: -------------------------------------------------------------------------------- 1 | # comet and clients protocols 2 | comet supports two protocols to communicate with client: WebSocket, TCP 3 | 4 | ## websocket 5 | **Request URL** 6 | 7 | ws://DOMAIN/sub 8 | 9 | **HTTP Request Method** 10 | 11 | WebSocket (JSON Frame). Response is same as the request. 12 | 13 | **Response Result** 14 | 15 | ```json 16 | { 17 | "ver": 102, 18 | "op": 10, 19 | "seq": 10, 20 | "body": {"data": "xxx"} 21 | } 22 | ``` 23 | 24 | **Request and Response Parameters** 25 | 26 | | parameter | is required | type | comment| 27 | | :----- | :--- | :--- | :--- | 28 | | ver | true | int | Protocol version | 29 | | op | true | int | Operation | 30 | | seq | true | int | Sequence number (Server returned number maps to client sent) | 31 | | body | json | The JSON message pushed | 32 | 33 | ## tcp 34 | **Request URL** 35 | 36 | tcp://DOMAIN 37 | 38 | **Protocol** 39 | 40 | Binary. Response is same as the request. 41 | 42 | **Request and Response Parameters** 43 | 44 | | parameter | is required | type | comment| 45 | | :----- | :--- | :--- | :--- | 46 | | package length | true | int32 bigendian | package length | 47 | | header Length | true | int16 bigendian | header length | 48 | | ver | true | int16 bigendian | Protocol version | 49 | | operation | true | int32 bigendian | Operation | 50 | | seq | true | int32 bigendian | jsonp callback | 51 | | body | false | binary | $(package lenth) - $(header length) | 52 | 53 | ## Operations 54 | | operation | comment | 55 | | :----- | :--- | 56 | | 2 | Client send heartbeat| 57 | | 3 | Server reply heartbeat| 58 | | 7 | authentication request | 59 | | 8 | authentication response | 60 | 61 | -------------------------------------------------------------------------------- /docs/en/push.md: -------------------------------------------------------------------------------- 1 |

Terry-Mao/goim push HTTP protocols

2 | push HTTP interface protocols for pusher 3 | 4 |

Interfaces

5 | | Name | URL | HTTP method | 6 | | :---- | :---- | :---- | 7 | | [single push](#single push) | /1/push | POST | 8 | | [multiple push](#multiple push) | /1/pushs | POST | 9 | | [room push](#room push) | /1/push/room | POST | 10 | | [broadcasting](#broadcasting) | /1/push/all | POST | 11 | 12 |

Public response body

13 | 14 | | response code | description | 15 | | :---- | :---- | 16 | | 1 | success | 17 | | 65535 | internal error | 18 | 19 |

Response structure

20 |
21 | {
22 |     "ret": 1  //response code
23 | }
24 | 
25 | 26 | 27 | ##### single push 28 | * Example request 29 | 30 | ```sh 31 | # uid is the user id pushing to?uid=0 32 | curl -d "{\"test\":1}" http://127.0.0.1:7172/1/push?uid=0 33 | ``` 34 | 35 | * Response 36 | 37 |
38 | {
39 |     "ret": 1
40 | }
41 | 
42 | 43 | ##### Multiple push 44 | * Example request 45 | 46 | ```sh 47 | curl -d "{\"u\":[1,2,3,4,5],\"m\":{\"test\":1}}" http://127.0.0.1:7172/1/pushs 48 | ``` 49 | 50 | * Response 51 | 52 |
53 | {
54 |     "ret": 1
55 | }
56 | 
57 | 58 | ##### room push 59 | * Example request 60 | 61 | ```sh 62 | curl -d "{\"test\": 1}" http://127.0.0.1:7172/1/push/room?rid=1 63 | ``` 64 | 65 | * Response 66 | 67 |
68 | {
69 |     "ret": 1
70 | }
71 | 
72 | 73 | ##### Broadcasting 74 | * Example request 75 | 76 | ```sh 77 | curl -d "{\"test\": 1}" http://127.0.0.1:7172/1/push/all 78 | ``` 79 | 80 | * Response 81 | 82 |
83 | {
84 |     "ret": 1
85 | }
86 | 
87 | -------------------------------------------------------------------------------- /docs/goim.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/goim.graffle -------------------------------------------------------------------------------- /docs/handshake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/handshake.png -------------------------------------------------------------------------------- /docs/proto.md: -------------------------------------------------------------------------------- 1 | # comet 客户端通讯协议文档 2 | comet支持两种协议和客户端通讯 websocket, tcp。 3 | 4 | ## websocket 5 | **请求URL** 6 | 7 | ws://DOMAIN/sub 8 | 9 | **HTTP请求方式** 10 | 11 | Websocket(JSON Frame),请求和返回协议一致 12 | 13 | **请求和返回json** 14 | 15 | ```json 16 | { 17 | "ver": 102, 18 | "op": 10, 19 | "seq": 10, 20 | "body": {"data": "xxx"} 21 | } 22 | ``` 23 | 24 | **请求和返回参数说明** 25 | 26 | | 参数名 | 必选 | 类型 | 说明 | 27 | | :----- | :--- | :--- | :--- | 28 | | ver | true | int | 协议版本号 | 29 | | op | true | int | 指令 | 30 | | seq | true | int | 序列号(服务端返回和客户端发送一一对应) | 31 | | body | true | string | 授权令牌,用于检验获取用户真实用户Id | 32 | 33 | ## tcp 34 | **请求URL** 35 | 36 | tcp://DOMAIN 37 | 38 | **协议格式** 39 | 40 | 二进制,请求和返回协议一致 41 | 42 | **请求&返回参数** 43 | 44 | | 参数名 | 必选 | 类型 | 说明 | 45 | | :----- | :--- | :--- | :--- | 46 | | package length | true | int32 bigendian | 包长度 | 47 | | header Length | true | int16 bigendian | 包头长度 | 48 | | ver | true | int16 bigendian | 协议版本 | 49 | | operation | true | int32 bigendian | 协议指令 | 50 | | seq | true | int32 bigendian | 序列号 | 51 | | body | false | binary | $(package lenth) - $(header length) | 52 | 53 | ## 指令 54 | | 指令 | 说明 | 55 | | :----- | :--- | 56 | | 2 | 客户端请求心跳 | 57 | | 3 | 服务端心跳答复 | 58 | | 5 | 下行消息 | 59 | | 7 | auth认证 | 60 | | 8 | auth认证返回 | 61 | 62 | -------------------------------------------------------------------------------- /docs/protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terry-Mao/goim/1800484a45334ace0bfa63a56ab9cbbbb4b22dca/docs/protocol.png -------------------------------------------------------------------------------- /docs/push.md: -------------------------------------------------------------------------------- 1 | ## goim push API 2 | 3 | ### error codes 4 | ``` 5 | // ok 6 | OK = 0 7 | 8 | // request error 9 | RequestErr = -400 10 | 11 | // server error 12 | ServerErr = -500 13 | ``` 14 | 15 | ### push keys 16 | [POST] /goim/push/keys 17 | 18 | | Name | Type | Remork | 19 | |:----------------|:--------:|:-----------------------| 20 | | [url]:operation | int32 | operation for response | 21 | | [url]:keys | []string | multiple client keys | 22 | | [Body] | []byte | http request body | 23 | 24 | response: 25 | ``` 26 | { 27 | "code": 0 28 | } 29 | ``` 30 | 31 | ### push mids 32 | [POST] /goim/push/mids 33 | 34 | | Name | Type | Remork | 35 | |:----------------|:--------:|:-----------------------| 36 | | [url]:operation | int32 | operation for response | 37 | | [url]:mids | []int64 | multiple user mids | 38 | | [Body] | []byte | http request body | 39 | 40 | response: 41 | ``` 42 | { 43 | "code": 0 44 | } 45 | ``` 46 | 47 | ### push room 48 | [POST] /goim/push/room 49 | 50 | | Name | Type | Remork | 51 | |:----------------|:--------:|:-----------------------| 52 | | [url]:operation | int32 | operation for response | 53 | | [url]:type | string | room type | 54 | | [url]:room | string | room id | 55 | | [Body] | []byte | http request body | 56 | 57 | response: 58 | ``` 59 | { 60 | "code": 0 61 | } 62 | ``` 63 | 64 | ### push all 65 | [POST] /goim/push/all 66 | 67 | | Name | Type | Remork | 68 | |:----------------|:--------:|:-----------------------| 69 | | [url]:operation | int32 | operation for response | 70 | | [url]:speed | int32 | push speed | 71 | | [Body] | []byte | http request body | 72 | 73 | response: 74 | ``` 75 | { 76 | "code": 0 77 | } 78 | ``` 79 | 80 | ### online top 81 | [GET] /goim/online/top 82 | 83 | | Name | Type | Remork | 84 | |:--------|:--------:|:-----------------------| 85 | | type | string | room type | 86 | | limit | string | online limit | 87 | 88 | response: 89 | ``` 90 | { 91 | "code": 0, 92 | "message": "", 93 | "data": [ 94 | { 95 | "room_id": "1000", 96 | "count": 100 97 | }, 98 | { 99 | "room_id": "2000", 100 | "count": 200 101 | }, 102 | { 103 | "room_id": "3000", 104 | "count": 300 105 | } 106 | ] 107 | } 108 | ``` 109 | 110 | ### online room 111 | [GET] /goim/online/room 112 | 113 | | Name | Type | Remork | 114 | |:--------|:--------:|:-----------------------| 115 | | type | string | room type | 116 | | rooms | []string | room ids | 117 | 118 | response: 119 | ``` 120 | { 121 | "code": 0, 122 | "message": "", 123 | "data": { 124 | "1000": 100, 125 | "2000": 200, 126 | "3000": 300 127 | } 128 | } 129 | ``` 130 | ### online total 131 | [GET] /goim/online/total 132 | 133 | response: 134 | ``` 135 | { 136 | "code": 0, 137 | "message": "", 138 | "data": { 139 | "conn_count": 1, 140 | "ip_count": 1 141 | } 142 | } 143 | ``` 144 | 145 | ### nodes weighted 146 | [GET] /goim/nodes/weighted 147 | 148 | | Name | Type | Remork | 149 | |:---------|:--------:|:-----------------------| 150 | | platform | string | web/android/ios | 151 | 152 | response: 153 | ``` 154 | { 155 | "code": 0, 156 | "message": "", 157 | "data": { 158 | "domain": "conn.goim.io", 159 | "tcp_port": 3101, 160 | "ws_port": 3102, 161 | "wss_port": 3103, 162 | "heartbeat": 30, // heartbeat seconds 163 | "heartbeat_max": 3 // heartbeat tries 164 | "nodes": [ 165 | "47.89.10.97" 166 | ], 167 | "backoff": { 168 | "max_delay": 300, 169 | "base_delay": 3, 170 | "factor": 1.8, 171 | "jitter": 0.3 172 | }, 173 | 174 | } 175 | } 176 | ``` 177 | 178 | ### nodes instances 179 | [GET] /nodes/instances 180 | 181 | response: 182 | ``` 183 | { 184 | "code": 0, 185 | "message": "", 186 | "data": [ 187 | { 188 | "region": "sh", 189 | "zone": "sh001", 190 | "env": "dev", 191 | "appid": "goim.comet", 192 | "hostname": "test", 193 | "addrs": [ 194 | "grpc://192.168.1.30:3109" 195 | ], 196 | "version": "", 197 | "latest_timestamp": 1545750122311688676, 198 | "metadata": { 199 | "addrs": "47.89.10.97", 200 | "conn_count": "1", 201 | "ip_count": "1", 202 | "offline": "false", 203 | "weight": "10" 204 | } 205 | } 206 | ] 207 | } 208 | ` 209 | -------------------------------------------------------------------------------- /examples/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID3TCCAsWgAwIBAgIJAKWU8wETRh4fMA0GCSqGSIb3DQEBBQUAMIGEMQswCQYD 3 | VQQGEwJjbjERMA8GA1UECAwIc2hhbmdoYWkxETAPBgNVBAcMCHNoYW5naGFpMQ0w 4 | CwYDVQQKDARiaWxpMQ0wCwYDVQQLDARiaWxpMREwDwYDVQQDDAhiaWxpLmNvbTEe 5 | MBwGCSqGSIb3DQEJARYPMjI0MzAzNTRAcXEuY29tMB4XDTE1MDkwMTEyMDQxMloX 6 | DTI1MDgyOTEyMDQxMlowgYQxCzAJBgNVBAYTAmNuMREwDwYDVQQIDAhzaGFuZ2hh 7 | aTERMA8GA1UEBwwIc2hhbmdoYWkxDTALBgNVBAoMBGJpbGkxDTALBgNVBAsMBGJp 8 | bGkxETAPBgNVBAMMCGJpbGkuY29tMR4wHAYJKoZIhvcNAQkBFg8yMjQzMDM1NEBx 9 | cS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKFwSxJqYPxzMe 10 | m5PeYA4YmcUsDCqS9Z7PsszOMZ1YsWZIHMB74D49ad2R+9PoqlfNH1L9C4NFSBrF 11 | rhSkaLmFYxw9yeJ2EAPijASBgfxMFVrEJhu7SW86OPTVnHblU8UqQdnMFOqF49C9 12 | mCdbiGu/99BZVCL1WmlSQCWVEIzOgX+goxqHuwXUF58YUwr6WLtF0DuBcLUai1vB 13 | Pg+PJ2fLjSR2o0KJkPOd6+y90cgoxfyJFUHuUKyV8EU4VwHEIA9rVizprziqPx6c 14 | 9A9Ng0FpA2leSPLGYCjnDtKIOvbSOS8DMkRT55ujqoVrj0yiNWsuJlc/NbD6bS16 15 | fJjuLOtJAgMBAAGjUDBOMB0GA1UdDgQWBBQzxdSIYIkDABh98Cj6VeYasC7/STAf 16 | BgNVHSMEGDAWgBQzxdSIYIkDABh98Cj6VeYasC7/STAMBgNVHRMEBTADAQH/MA0G 17 | CSqGSIb3DQEBBQUAA4IBAQA1Fhr+SU62xHWlPOBhTbjod49+mNfXn2TZz/vBp/Jl 18 | pHZgDLAEcrhXHmi2A0G9K9+qOIEn4BvTd70jSYvYlaeUSzZ/nEpeM0oE0f2Qaxov 19 | PhxDpsqPsSQm6pE64/los1doaiElfMVFaP56UGV01kFdI013wxwd2WCuj51Hmvi9 20 | thsS027aqxjHMJnKXPvBm2E6EDkPfc/e+AEmwBzry+aamRizaMrk/SfSGTy9/rvd 21 | +VbBfHiJ50kMld51SLIc6qkVaTXess7mIfcsk7kyjP4eFA0y+3wmXfRZeWadND3I 22 | O9XNNwsDVFXlhW40GUVriy95qa1Sq3sLUfUQcCH4VFFK 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /examples/javascript/client.js: -------------------------------------------------------------------------------- 1 | (function(win) { 2 | const rawHeaderLen = 16; 3 | const packetOffset = 0; 4 | const headerOffset = 4; 5 | const verOffset = 6; 6 | const opOffset = 8; 7 | const seqOffset = 12; 8 | 9 | var Client = function(options) { 10 | var MAX_CONNECT_TIMES = 10; 11 | var DELAY = 15000; 12 | this.options = options || {}; 13 | this.createConnect(MAX_CONNECT_TIMES, DELAY); 14 | } 15 | 16 | var appendMsg = function(text) { 17 | var span = document.createElement("SPAN"); 18 | var text = document.createTextNode(text); 19 | span.appendChild(text); 20 | document.getElementById("box").appendChild(span); 21 | } 22 | 23 | Client.prototype.createConnect = function(max, delay) { 24 | var self = this; 25 | if (max === 0) { 26 | return; 27 | } 28 | connect(); 29 | 30 | var textDecoder = new TextDecoder(); 31 | var textEncoder = new TextEncoder(); 32 | var heartbeatInterval; 33 | function connect() { 34 | var ws = new WebSocket('ws://sh.tony.wiki:3102/sub'); 35 | //var ws = new WebSocket('ws://127.0.0.1:3102/sub'); 36 | ws.binaryType = 'arraybuffer'; 37 | ws.onopen = function() { 38 | auth(); 39 | } 40 | 41 | ws.onmessage = function(evt) { 42 | var data = evt.data; 43 | var dataView = new DataView(data, 0); 44 | var packetLen = dataView.getInt32(packetOffset); 45 | var headerLen = dataView.getInt16(headerOffset); 46 | var ver = dataView.getInt16(verOffset); 47 | var op = dataView.getInt32(opOffset); 48 | var seq = dataView.getInt32(seqOffset); 49 | 50 | console.log("receiveHeader: packetLen=" + packetLen, "headerLen=" + headerLen, "ver=" + ver, "op=" + op, "seq=" + seq); 51 | 52 | switch(op) { 53 | case 8: 54 | // auth reply ok 55 | document.getElementById("status").innerHTML = "ok"; 56 | appendMsg("receive: auth reply"); 57 | // send a heartbeat to server 58 | heartbeat(); 59 | heartbeatInterval = setInterval(heartbeat, 30 * 1000); 60 | break; 61 | case 3: 62 | // receive a heartbeat from server 63 | console.log("receive: heartbeat"); 64 | appendMsg("receive: heartbeat reply"); 65 | break; 66 | case 9: 67 | // batch message 68 | for (var offset=rawHeaderLen; offset 2 | 3 | 4 | 5 | 6 | client demo 7 | 8 | 16 | 19 | 20 | 21 |

websocket

22 |

status:

23 | 24 |

push

25 |
26 |

curl -d 'mid message' 'http://api.goim.io:3111/goim/push/mids?operation=1000&mids=123'

27 |

curl -d 'room message' 'http://api.goim.io:3111/goim/push/room?operation=1000&type=live&room=1000'

28 |

curl -d 'broadcast message' 'http://api.goim.io:3111/goim/push/all?operation=1000'

29 |
30 |

message:

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/javascript/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | // Simple static webserver: 10 | log.Fatal(http.ListenAndServe(":1999", http.FileServer(http.Dir("./")))) 11 | } 12 | -------------------------------------------------------------------------------- /examples/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAyhcEsSamD8czHpuT3mAOGJnFLAwqkvWez7LMzjGdWLFmSBzA 3 | e+A+PWndkfvT6KpXzR9S/QuDRUgaxa4UpGi5hWMcPcnidhAD4owEgYH8TBVaxCYb 4 | u0lvOjj01Zx25VPFKkHZzBTqhePQvZgnW4hrv/fQWVQi9VppUkAllRCMzoF/oKMa 5 | h7sF1BefGFMK+li7RdA7gXC1GotbwT4Pjydny40kdqNCiZDznevsvdHIKMX8iRVB 6 | 7lCslfBFOFcBxCAPa1Ys6a84qj8enPQPTYNBaQNpXkjyxmAo5w7SiDr20jkvAzJE 7 | U+ebo6qFa49MojVrLiZXPzWw+m0tenyY7izrSQIDAQABAoIBAQCNasAwy2/nmKjw 8 | IUS/l44lru1oXncocdMZWvCw1c1a9IEzs1MLHKfRSBTyBDyNEy7v7pyfUQAiaku5 9 | y5DMYDB65BkuL+lWXuypCvxYOEL6ZvMmUdiUHdZE8vh5xsz4u788S+qCQpy+5uX6 10 | 1s+r4PIt2tekuxjfgs4y7YqfHn66PmxAWXTV+jnWpUWYjpRTgDWeU6ikbk2fHjvG 11 | ytF4AV4lEKNnGBC5F+5lk3QJKjb1IW/o5q42TCeERk1k5Ruc5kfzq72hdFYrQAF1 12 | PmstEXMExMS/K6AerYtPfiWWFWH2gCTZMf/C8Jwr0zPVpHM1PF+Ien3IkgiOTfZE 13 | 1+/wrx4BAoGBAOnpnxFnnqhZdDRi+DALfY3mXPwOizFYnAhuuuzO87zoZsps529E 14 | j5pEZQoYPTiww5rptjFhNjoV0gsh2GO5QHCiMxM8A76aWVc4YmK0GiHVudpAu2t+ 15 | aK8+0Xr03SA27cKJjp12NWijzEdTjSQYaFwD/8/dMrU3MCu30kdEJ6iJAoGBAN0s 16 | JXLv4CtB/DLTvifTZ/OAGSc1/Z+X0w2EqIugw7IV9YuAm2721IFhsFFXeANRRHY6 17 | zonLUEgsuQUc30NCz+susChs2GMEFypLem8D1c5ZZY+9sIsR1WFB97o7bTMvp5lH 18 | 2REpEDZKXVOB7UmrclLgTKRQgG3O73rMP6QXYPzBAoGBAJm8Gvi0crlQuagol9fz 19 | 5Vwa2GgtItyW0U5VgHNdfSJeWBiYxO8DT6Jja0jcL3iP7K9nBYCk1KAOcVMxtmes 20 | fKbKY+kzW36tMSS7ASbAGiC8uH6yZru6hBERp1o5jw+6Kj/eaqYg5+9TIFKMnknn 21 | 5Mb9NecnCUnC8Nz63rBKIgqJAoGBAJHUpeyfFaPwIiYxT1RbJFN9xxf/lXdBWDu1 22 | mJxYKDCoIfsVlWcZAQ0+KE+56LvnPcjnBX/9urWcJ3KjkuJ6jzV211gQTK0c6VlN 23 | 4zCHytYAQ+L/JATOgW9bW8hDnsD9TvjWUt3pwXLKnbaOGLNWhE747g/5tHSy2VyS 24 | h/PeJmkBAoGABMfEaiLHXXQhUK0BPYxT3T8i9IjAYwXlrgSLlnvOGslZef/45kP5 25 | CM1UbMSwAn+HvAziOFt2WmynFCysy/lCyTud+Fd/IZFcMThp7wvi7fSZo0NDM7ES 26 | 9JfgTmCY4Kwv6kT85poIka9bp4Nh47EVB9kDoqm/lSMkfYqcWH66DJA= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Terry-Mao/goim 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/Shopify/sarama v1.19.0 // indirect 8 | github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect 9 | github.com/bilibili/discovery v1.0.1 10 | github.com/bsm/sarama-cluster v2.1.15+incompatible 11 | github.com/eapache/go-resiliency v1.1.0 // indirect 12 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect 13 | github.com/eapache/queue v1.1.0 // indirect 14 | github.com/gin-gonic/gin v1.7.0 15 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 16 | github.com/golang/protobuf v1.4.3 17 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect 18 | github.com/gomodule/redigo v2.0.0+incompatible 19 | github.com/google/uuid v1.0.0 20 | github.com/onsi/ginkgo v1.16.0 // indirect 21 | github.com/onsi/gomega v1.11.0 // indirect 22 | github.com/pierrec/lz4 v2.0.5+incompatible // indirect 23 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect 24 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect 25 | github.com/stretchr/testify v1.5.1 26 | github.com/zhenjl/cityhash v0.0.0-20131128155616-cdd6a94144ab 27 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 28 | google.golang.org/grpc v1.22.3 29 | gopkg.in/Shopify/sarama.v1 v1.19.0 30 | ) 31 | -------------------------------------------------------------------------------- /internal/comet/bucket.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | pb "github.com/Terry-Mao/goim/api/comet" 8 | "github.com/Terry-Mao/goim/api/protocol" 9 | "github.com/Terry-Mao/goim/internal/comet/conf" 10 | ) 11 | 12 | // Bucket is a channel holder. 13 | type Bucket struct { 14 | c *conf.Bucket 15 | cLock sync.RWMutex // protect the channels for chs 16 | chs map[string]*Channel // map sub key to a channel 17 | // room 18 | rooms map[string]*Room // bucket room channels 19 | routines []chan *pb.BroadcastRoomReq 20 | routinesNum uint64 21 | 22 | ipCnts map[string]int32 23 | } 24 | 25 | // NewBucket new a bucket struct. store the key with im channel. 26 | func NewBucket(c *conf.Bucket) (b *Bucket) { 27 | b = new(Bucket) 28 | b.chs = make(map[string]*Channel, c.Channel) 29 | b.ipCnts = make(map[string]int32) 30 | b.c = c 31 | b.rooms = make(map[string]*Room, c.Room) 32 | b.routines = make([]chan *pb.BroadcastRoomReq, c.RoutineAmount) 33 | for i := uint64(0); i < c.RoutineAmount; i++ { 34 | c := make(chan *pb.BroadcastRoomReq, c.RoutineSize) 35 | b.routines[i] = c 36 | go b.roomproc(c) 37 | } 38 | return 39 | } 40 | 41 | // ChannelCount channel count in the bucket 42 | func (b *Bucket) ChannelCount() int { 43 | return len(b.chs) 44 | } 45 | 46 | // RoomCount room count in the bucket 47 | func (b *Bucket) RoomCount() int { 48 | return len(b.rooms) 49 | } 50 | 51 | // RoomsCount get all room id where online number > 0. 52 | func (b *Bucket) RoomsCount() (res map[string]int32) { 53 | var ( 54 | roomID string 55 | room *Room 56 | ) 57 | b.cLock.RLock() 58 | res = make(map[string]int32) 59 | for roomID, room = range b.rooms { 60 | if room.Online > 0 { 61 | res[roomID] = room.Online 62 | } 63 | } 64 | b.cLock.RUnlock() 65 | return 66 | } 67 | 68 | // ChangeRoom change ro room 69 | func (b *Bucket) ChangeRoom(nrid string, ch *Channel) (err error) { 70 | var ( 71 | nroom *Room 72 | ok bool 73 | oroom = ch.Room 74 | ) 75 | // change to no room 76 | if nrid == "" { 77 | if oroom != nil && oroom.Del(ch) { 78 | b.DelRoom(oroom) 79 | } 80 | ch.Room = nil 81 | return 82 | } 83 | b.cLock.Lock() 84 | if nroom, ok = b.rooms[nrid]; !ok { 85 | nroom = NewRoom(nrid) 86 | b.rooms[nrid] = nroom 87 | } 88 | b.cLock.Unlock() 89 | if oroom != nil && oroom.Del(ch) { 90 | b.DelRoom(oroom) 91 | } 92 | 93 | if err = nroom.Put(ch); err != nil { 94 | return 95 | } 96 | ch.Room = nroom 97 | return 98 | } 99 | 100 | // Put put a channel according with sub key. 101 | func (b *Bucket) Put(rid string, ch *Channel) (err error) { 102 | var ( 103 | room *Room 104 | ok bool 105 | ) 106 | b.cLock.Lock() 107 | // close old channel 108 | if dch := b.chs[ch.Key]; dch != nil { 109 | dch.Close() 110 | } 111 | b.chs[ch.Key] = ch 112 | if rid != "" { 113 | if room, ok = b.rooms[rid]; !ok { 114 | room = NewRoom(rid) 115 | b.rooms[rid] = room 116 | } 117 | ch.Room = room 118 | } 119 | b.ipCnts[ch.IP]++ 120 | b.cLock.Unlock() 121 | if room != nil { 122 | err = room.Put(ch) 123 | } 124 | return 125 | } 126 | 127 | // Del delete the channel by sub key. 128 | func (b *Bucket) Del(dch *Channel) { 129 | room := dch.Room 130 | b.cLock.Lock() 131 | if ch, ok := b.chs[dch.Key]; ok { 132 | if ch == dch { 133 | delete(b.chs, ch.Key) 134 | } 135 | // ip counter 136 | if b.ipCnts[ch.IP] > 1 { 137 | b.ipCnts[ch.IP]-- 138 | } else { 139 | delete(b.ipCnts, ch.IP) 140 | } 141 | } 142 | b.cLock.Unlock() 143 | if room != nil && room.Del(dch) { 144 | // if empty room, must delete from bucket 145 | b.DelRoom(room) 146 | } 147 | } 148 | 149 | // Channel get a channel by sub key. 150 | func (b *Bucket) Channel(key string) (ch *Channel) { 151 | b.cLock.RLock() 152 | ch = b.chs[key] 153 | b.cLock.RUnlock() 154 | return 155 | } 156 | 157 | // Broadcast push msgs to all channels in the bucket. 158 | func (b *Bucket) Broadcast(p *protocol.Proto, op int32) { 159 | var ch *Channel 160 | b.cLock.RLock() 161 | for _, ch = range b.chs { 162 | if !ch.NeedPush(op) { 163 | continue 164 | } 165 | _ = ch.Push(p) 166 | } 167 | b.cLock.RUnlock() 168 | } 169 | 170 | // Room get a room by roomid. 171 | func (b *Bucket) Room(rid string) (room *Room) { 172 | b.cLock.RLock() 173 | room = b.rooms[rid] 174 | b.cLock.RUnlock() 175 | return 176 | } 177 | 178 | // DelRoom delete a room by roomid. 179 | func (b *Bucket) DelRoom(room *Room) { 180 | b.cLock.Lock() 181 | delete(b.rooms, room.ID) 182 | b.cLock.Unlock() 183 | room.Close() 184 | } 185 | 186 | // BroadcastRoom broadcast a message to specified room 187 | func (b *Bucket) BroadcastRoom(arg *pb.BroadcastRoomReq) { 188 | num := atomic.AddUint64(&b.routinesNum, 1) % b.c.RoutineAmount 189 | b.routines[num] <- arg 190 | } 191 | 192 | // Rooms get all room id where online number > 0. 193 | func (b *Bucket) Rooms() (res map[string]struct{}) { 194 | var ( 195 | roomID string 196 | room *Room 197 | ) 198 | res = make(map[string]struct{}) 199 | b.cLock.RLock() 200 | for roomID, room = range b.rooms { 201 | if room.Online > 0 { 202 | res[roomID] = struct{}{} 203 | } 204 | } 205 | b.cLock.RUnlock() 206 | return 207 | } 208 | 209 | // IPCount get ip count. 210 | func (b *Bucket) IPCount() (res map[string]struct{}) { 211 | var ( 212 | ip string 213 | ) 214 | b.cLock.RLock() 215 | res = make(map[string]struct{}, len(b.ipCnts)) 216 | for ip = range b.ipCnts { 217 | res[ip] = struct{}{} 218 | } 219 | b.cLock.RUnlock() 220 | return 221 | } 222 | 223 | // UpRoomsCount update all room count 224 | func (b *Bucket) UpRoomsCount(roomCountMap map[string]int32) { 225 | var ( 226 | roomID string 227 | room *Room 228 | ) 229 | b.cLock.RLock() 230 | for roomID, room = range b.rooms { 231 | room.AllOnline = roomCountMap[roomID] 232 | } 233 | b.cLock.RUnlock() 234 | } 235 | 236 | // roomproc 237 | func (b *Bucket) roomproc(c chan *pb.BroadcastRoomReq) { 238 | for { 239 | arg := <-c 240 | if room := b.Room(arg.RoomID); room != nil { 241 | room.Push(arg.Proto) 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /internal/comet/channel.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Terry-Mao/goim/api/protocol" 7 | "github.com/Terry-Mao/goim/internal/comet/errors" 8 | "github.com/Terry-Mao/goim/pkg/bufio" 9 | ) 10 | 11 | // Channel used by message pusher send msg to write goroutine. 12 | type Channel struct { 13 | Room *Room 14 | CliProto Ring 15 | signal chan *protocol.Proto 16 | Writer bufio.Writer 17 | Reader bufio.Reader 18 | Next *Channel 19 | Prev *Channel 20 | 21 | Mid int64 22 | Key string 23 | IP string 24 | watchOps map[int32]struct{} 25 | mutex sync.RWMutex 26 | } 27 | 28 | // NewChannel new a channel. 29 | func NewChannel(cli, svr int) *Channel { 30 | c := new(Channel) 31 | c.CliProto.Init(cli) 32 | c.signal = make(chan *protocol.Proto, svr) 33 | c.watchOps = make(map[int32]struct{}) 34 | return c 35 | } 36 | 37 | // Watch watch a operation. 38 | func (c *Channel) Watch(accepts ...int32) { 39 | c.mutex.Lock() 40 | for _, op := range accepts { 41 | c.watchOps[op] = struct{}{} 42 | } 43 | c.mutex.Unlock() 44 | } 45 | 46 | // UnWatch unwatch an operation 47 | func (c *Channel) UnWatch(accepts ...int32) { 48 | c.mutex.Lock() 49 | for _, op := range accepts { 50 | delete(c.watchOps, op) 51 | } 52 | c.mutex.Unlock() 53 | } 54 | 55 | // NeedPush verify if in watch. 56 | func (c *Channel) NeedPush(op int32) bool { 57 | c.mutex.RLock() 58 | if _, ok := c.watchOps[op]; ok { 59 | c.mutex.RUnlock() 60 | return true 61 | } 62 | c.mutex.RUnlock() 63 | return false 64 | } 65 | 66 | // Push server push message. 67 | func (c *Channel) Push(p *protocol.Proto) (err error) { 68 | select { 69 | case c.signal <- p: 70 | default: 71 | err = errors.ErrSignalFullMsgDropped 72 | } 73 | return 74 | } 75 | 76 | // Ready check the channel ready or close? 77 | func (c *Channel) Ready() *protocol.Proto { 78 | return <-c.signal 79 | } 80 | 81 | // Signal send signal to the channel, protocol ready. 82 | func (c *Channel) Signal() { 83 | c.signal <- protocol.ProtoReady 84 | } 85 | 86 | // Close close the channel. 87 | func (c *Channel) Close() { 88 | c.signal <- protocol.ProtoFinish 89 | } 90 | -------------------------------------------------------------------------------- /internal/comet/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bilibili/discovery/naming" 11 | "github.com/BurntSushi/toml" 12 | xtime "github.com/Terry-Mao/goim/pkg/time" 13 | ) 14 | 15 | var ( 16 | confPath string 17 | region string 18 | zone string 19 | deployEnv string 20 | host string 21 | addrs string 22 | weight int64 23 | offline bool 24 | debug bool 25 | 26 | // Conf config 27 | Conf *Config 28 | ) 29 | 30 | func init() { 31 | var ( 32 | defHost, _ = os.Hostname() 33 | defAddrs = os.Getenv("ADDRS") 34 | defWeight, _ = strconv.ParseInt(os.Getenv("WEIGHT"), 10, 32) 35 | defOffline, _ = strconv.ParseBool(os.Getenv("OFFLINE")) 36 | defDebug, _ = strconv.ParseBool(os.Getenv("DEBUG")) 37 | ) 38 | flag.StringVar(&confPath, "conf", "comet-example.toml", "default config path.") 39 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.") 40 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.") 41 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.") 42 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.") 43 | flag.StringVar(&addrs, "addrs", defAddrs, "server public ip addrs. or use ADDRS env variable, value: 127.0.0.1 etc.") 44 | flag.Int64Var(&weight, "weight", defWeight, "load balancing weight, or use WEIGHT env variable, value: 10 etc.") 45 | flag.BoolVar(&offline, "offline", defOffline, "server offline. or use OFFLINE env variable, value: true/false etc.") 46 | flag.BoolVar(&debug, "debug", defDebug, "server debug. or use DEBUG env variable, value: true/false etc.") 47 | } 48 | 49 | // Init init config. 50 | func Init() (err error) { 51 | Conf = Default() 52 | _, err = toml.DecodeFile(confPath, &Conf) 53 | return 54 | } 55 | 56 | // Default new a config with specified defualt value. 57 | func Default() *Config { 58 | return &Config{ 59 | Debug: debug, 60 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host, Weight: weight, Addrs: strings.Split(addrs, ","), Offline: offline}, 61 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host}, 62 | RPCClient: &RPCClient{ 63 | Dial: xtime.Duration(time.Second), 64 | Timeout: xtime.Duration(time.Second), 65 | }, 66 | RPCServer: &RPCServer{ 67 | Network: "tcp", 68 | Addr: ":3109", 69 | Timeout: xtime.Duration(time.Second), 70 | IdleTimeout: xtime.Duration(time.Second * 60), 71 | MaxLifeTime: xtime.Duration(time.Hour * 2), 72 | ForceCloseWait: xtime.Duration(time.Second * 20), 73 | KeepAliveInterval: xtime.Duration(time.Second * 60), 74 | KeepAliveTimeout: xtime.Duration(time.Second * 20), 75 | }, 76 | TCP: &TCP{ 77 | Bind: []string{":3101"}, 78 | Sndbuf: 4096, 79 | Rcvbuf: 4096, 80 | KeepAlive: false, 81 | Reader: 32, 82 | ReadBuf: 1024, 83 | ReadBufSize: 8192, 84 | Writer: 32, 85 | WriteBuf: 1024, 86 | WriteBufSize: 8192, 87 | }, 88 | Websocket: &Websocket{ 89 | Bind: []string{":3102"}, 90 | }, 91 | Protocol: &Protocol{ 92 | Timer: 32, 93 | TimerSize: 2048, 94 | CliProto: 5, 95 | SvrProto: 10, 96 | HandshakeTimeout: xtime.Duration(time.Second * 5), 97 | }, 98 | Bucket: &Bucket{ 99 | Size: 32, 100 | Channel: 1024, 101 | Room: 1024, 102 | RoutineAmount: 32, 103 | RoutineSize: 1024, 104 | }, 105 | } 106 | } 107 | 108 | // Config is comet config. 109 | type Config struct { 110 | Debug bool 111 | Env *Env 112 | Discovery *naming.Config 113 | TCP *TCP 114 | Websocket *Websocket 115 | Protocol *Protocol 116 | Bucket *Bucket 117 | RPCClient *RPCClient 118 | RPCServer *RPCServer 119 | Whitelist *Whitelist 120 | } 121 | 122 | // Env is env config. 123 | type Env struct { 124 | Region string 125 | Zone string 126 | DeployEnv string 127 | Host string 128 | Weight int64 129 | Offline bool 130 | Addrs []string 131 | } 132 | 133 | // RPCClient is RPC client config. 134 | type RPCClient struct { 135 | Dial xtime.Duration 136 | Timeout xtime.Duration 137 | } 138 | 139 | // RPCServer is RPC server config. 140 | type RPCServer struct { 141 | Network string 142 | Addr string 143 | Timeout xtime.Duration 144 | IdleTimeout xtime.Duration 145 | MaxLifeTime xtime.Duration 146 | ForceCloseWait xtime.Duration 147 | KeepAliveInterval xtime.Duration 148 | KeepAliveTimeout xtime.Duration 149 | } 150 | 151 | // TCP is tcp config. 152 | type TCP struct { 153 | Bind []string 154 | Sndbuf int 155 | Rcvbuf int 156 | KeepAlive bool 157 | Reader int 158 | ReadBuf int 159 | ReadBufSize int 160 | Writer int 161 | WriteBuf int 162 | WriteBufSize int 163 | } 164 | 165 | // Websocket is websocket config. 166 | type Websocket struct { 167 | Bind []string 168 | TLSOpen bool 169 | TLSBind []string 170 | CertFile string 171 | PrivateFile string 172 | } 173 | 174 | // Protocol is protocol config. 175 | type Protocol struct { 176 | Timer int 177 | TimerSize int 178 | SvrProto int 179 | CliProto int 180 | HandshakeTimeout xtime.Duration 181 | } 182 | 183 | // Bucket is bucket config. 184 | type Bucket struct { 185 | Size int 186 | Channel int 187 | Room int 188 | RoutineAmount uint64 189 | RoutineSize int 190 | } 191 | 192 | // Whitelist is white list config. 193 | type Whitelist struct { 194 | Whitelist []int64 195 | WhiteLog string 196 | } 197 | -------------------------------------------------------------------------------- /internal/comet/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // . 8 | var ( 9 | // server 10 | ErrHandshake = errors.New("handshake failed") 11 | ErrOperation = errors.New("request operation not valid") 12 | // ring 13 | ErrRingEmpty = errors.New("ring buffer empty") 14 | ErrRingFull = errors.New("ring buffer full") 15 | // timer 16 | ErrTimerFull = errors.New("timer full") 17 | ErrTimerEmpty = errors.New("timer empty") 18 | ErrTimerNoItem = errors.New("timer item not exist") 19 | // channel 20 | ErrPushMsgArg = errors.New("rpc pushmsg arg error") 21 | ErrPushMsgsArg = errors.New("rpc pushmsgs arg error") 22 | ErrMPushMsgArg = errors.New("rpc mpushmsg arg error") 23 | ErrMPushMsgsArg = errors.New("rpc mpushmsgs arg error") 24 | ErrSignalFullMsgDropped = errors.New("signal channel full, msg dropped") 25 | // bucket 26 | ErrBroadCastArg = errors.New("rpc broadcast arg error") 27 | ErrBroadCastRoomArg = errors.New("rpc broadcast room arg error") 28 | 29 | // room 30 | ErrRoomDroped = errors.New("room droped") 31 | // rpc 32 | ErrLogic = errors.New("logic rpc is not available") 33 | ) 34 | -------------------------------------------------------------------------------- /internal/comet/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | pb "github.com/Terry-Mao/goim/api/comet" 9 | "github.com/Terry-Mao/goim/internal/comet" 10 | "github.com/Terry-Mao/goim/internal/comet/conf" 11 | "github.com/Terry-Mao/goim/internal/comet/errors" 12 | 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/keepalive" 15 | ) 16 | 17 | // New comet grpc server. 18 | func New(c *conf.RPCServer, s *comet.Server) *grpc.Server { 19 | keepParams := grpc.KeepaliveParams(keepalive.ServerParameters{ 20 | MaxConnectionIdle: time.Duration(c.IdleTimeout), 21 | MaxConnectionAgeGrace: time.Duration(c.ForceCloseWait), 22 | Time: time.Duration(c.KeepAliveInterval), 23 | Timeout: time.Duration(c.KeepAliveTimeout), 24 | MaxConnectionAge: time.Duration(c.MaxLifeTime), 25 | }) 26 | srv := grpc.NewServer(keepParams) 27 | pb.RegisterCometServer(srv, &server{s}) 28 | lis, err := net.Listen(c.Network, c.Addr) 29 | if err != nil { 30 | panic(err) 31 | } 32 | go func() { 33 | if err := srv.Serve(lis); err != nil { 34 | panic(err) 35 | } 36 | }() 37 | return srv 38 | } 39 | 40 | type server struct { 41 | srv *comet.Server 42 | } 43 | 44 | var _ pb.CometServer = &server{} 45 | 46 | // PushMsg push a message to specified sub keys. 47 | func (s *server) PushMsg(ctx context.Context, req *pb.PushMsgReq) (reply *pb.PushMsgReply, err error) { 48 | if len(req.Keys) == 0 || req.Proto == nil { 49 | return nil, errors.ErrPushMsgArg 50 | } 51 | for _, key := range req.Keys { 52 | bucket := s.srv.Bucket(key) 53 | if bucket == nil { 54 | continue 55 | } 56 | if channel := bucket.Channel(key); channel != nil { 57 | if !channel.NeedPush(req.ProtoOp) { 58 | continue 59 | } 60 | if err = channel.Push(req.Proto); err != nil { 61 | return 62 | } 63 | } 64 | } 65 | return &pb.PushMsgReply{}, nil 66 | } 67 | 68 | // Broadcast broadcast msg to all user. 69 | func (s *server) Broadcast(ctx context.Context, req *pb.BroadcastReq) (*pb.BroadcastReply, error) { 70 | if req.Proto == nil { 71 | return nil, errors.ErrBroadCastArg 72 | } 73 | // TODO use broadcast queue 74 | go func() { 75 | for _, bucket := range s.srv.Buckets() { 76 | bucket.Broadcast(req.GetProto(), req.ProtoOp) 77 | if req.Speed > 0 { 78 | t := bucket.ChannelCount() / int(req.Speed) 79 | time.Sleep(time.Duration(t) * time.Second) 80 | } 81 | } 82 | }() 83 | return &pb.BroadcastReply{}, nil 84 | } 85 | 86 | // BroadcastRoom broadcast msg to specified room. 87 | func (s *server) BroadcastRoom(ctx context.Context, req *pb.BroadcastRoomReq) (*pb.BroadcastRoomReply, error) { 88 | if req.Proto == nil || req.RoomID == "" { 89 | return nil, errors.ErrBroadCastRoomArg 90 | } 91 | for _, bucket := range s.srv.Buckets() { 92 | bucket.BroadcastRoom(req) 93 | } 94 | return &pb.BroadcastRoomReply{}, nil 95 | } 96 | 97 | // Rooms gets all the room ids for the server. 98 | func (s *server) Rooms(ctx context.Context, req *pb.RoomsReq) (*pb.RoomsReply, error) { 99 | var ( 100 | roomIds = make(map[string]bool) 101 | ) 102 | for _, bucket := range s.srv.Buckets() { 103 | for roomID := range bucket.Rooms() { 104 | roomIds[roomID] = true 105 | } 106 | } 107 | return &pb.RoomsReply{Rooms: roomIds}, nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/comet/operation.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/Terry-Mao/goim/api/logic" 8 | "github.com/Terry-Mao/goim/api/protocol" 9 | "github.com/Terry-Mao/goim/pkg/strings" 10 | log "github.com/golang/glog" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/encoding/gzip" 14 | ) 15 | 16 | // Connect connected a connection. 17 | func (s *Server) Connect(c context.Context, p *protocol.Proto, cookie string) (mid int64, key, rid string, accepts []int32, heartbeat time.Duration, err error) { 18 | reply, err := s.rpcClient.Connect(c, &logic.ConnectReq{ 19 | Server: s.serverID, 20 | Cookie: cookie, 21 | Token: p.Body, 22 | }) 23 | if err != nil { 24 | return 25 | } 26 | return reply.Mid, reply.Key, reply.RoomID, reply.Accepts, time.Duration(reply.Heartbeat), nil 27 | } 28 | 29 | // Disconnect disconnected a connection. 30 | func (s *Server) Disconnect(c context.Context, mid int64, key string) (err error) { 31 | _, err = s.rpcClient.Disconnect(context.Background(), &logic.DisconnectReq{ 32 | Server: s.serverID, 33 | Mid: mid, 34 | Key: key, 35 | }) 36 | return 37 | } 38 | 39 | // Heartbeat heartbeat a connection session. 40 | func (s *Server) Heartbeat(ctx context.Context, mid int64, key string) (err error) { 41 | _, err = s.rpcClient.Heartbeat(ctx, &logic.HeartbeatReq{ 42 | Server: s.serverID, 43 | Mid: mid, 44 | Key: key, 45 | }) 46 | return 47 | } 48 | 49 | // RenewOnline renew room online. 50 | func (s *Server) RenewOnline(ctx context.Context, serverID string, roomCount map[string]int32) (allRoom map[string]int32, err error) { 51 | reply, err := s.rpcClient.RenewOnline(ctx, &logic.OnlineReq{ 52 | Server: s.serverID, 53 | RoomCount: roomCount, 54 | }, grpc.UseCompressor(gzip.Name)) 55 | if err != nil { 56 | return 57 | } 58 | return reply.AllRoomCount, nil 59 | } 60 | 61 | // Receive receive a message. 62 | func (s *Server) Receive(ctx context.Context, mid int64, p *protocol.Proto) (err error) { 63 | _, err = s.rpcClient.Receive(ctx, &logic.ReceiveReq{Mid: mid, Proto: p}) 64 | return 65 | } 66 | 67 | // Operate operate. 68 | func (s *Server) Operate(ctx context.Context, p *protocol.Proto, ch *Channel, b *Bucket) error { 69 | switch p.Op { 70 | case protocol.OpChangeRoom: 71 | if err := b.ChangeRoom(string(p.Body), ch); err != nil { 72 | log.Errorf("b.ChangeRoom(%s) error(%v)", p.Body, err) 73 | } 74 | p.Op = protocol.OpChangeRoomReply 75 | case protocol.OpSub: 76 | if ops, err := strings.SplitInt32s(string(p.Body), ","); err == nil { 77 | ch.Watch(ops...) 78 | } 79 | p.Op = protocol.OpSubReply 80 | case protocol.OpUnsub: 81 | if ops, err := strings.SplitInt32s(string(p.Body), ","); err == nil { 82 | ch.UnWatch(ops...) 83 | } 84 | p.Op = protocol.OpUnsubReply 85 | default: 86 | // TODO ack ok&failed 87 | if err := s.Receive(ctx, ch.Mid, p); err != nil { 88 | log.Errorf("s.Report(%d) op:%d error(%v)", ch.Mid, p.Op, err) 89 | } 90 | p.Body = nil 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/comet/ring.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "github.com/Terry-Mao/goim/api/protocol" 5 | "github.com/Terry-Mao/goim/internal/comet/conf" 6 | "github.com/Terry-Mao/goim/internal/comet/errors" 7 | log "github.com/golang/glog" 8 | ) 9 | 10 | // Ring ring proto buffer. 11 | type Ring struct { 12 | // read 13 | rp uint64 14 | num uint64 15 | mask uint64 16 | // TODO split cacheline, many cpu cache line size is 64 17 | // pad [40]byte 18 | // write 19 | wp uint64 20 | data []protocol.Proto 21 | } 22 | 23 | // NewRing new a ring buffer. 24 | func NewRing(num int) *Ring { 25 | r := new(Ring) 26 | r.init(uint64(num)) 27 | return r 28 | } 29 | 30 | // Init init ring. 31 | func (r *Ring) Init(num int) { 32 | r.init(uint64(num)) 33 | } 34 | 35 | func (r *Ring) init(num uint64) { 36 | // 2^N 37 | if num&(num-1) != 0 { 38 | for num&(num-1) != 0 { 39 | num &= num - 1 40 | } 41 | num <<= 1 42 | } 43 | r.data = make([]protocol.Proto, num) 44 | r.num = num 45 | r.mask = r.num - 1 46 | } 47 | 48 | // Get get a proto from ring. 49 | func (r *Ring) Get() (proto *protocol.Proto, err error) { 50 | if r.rp == r.wp { 51 | return nil, errors.ErrRingEmpty 52 | } 53 | proto = &r.data[r.rp&r.mask] 54 | return 55 | } 56 | 57 | // GetAdv incr read index. 58 | func (r *Ring) GetAdv() { 59 | r.rp++ 60 | if conf.Conf.Debug { 61 | log.Infof("ring rp: %d, idx: %d", r.rp, r.rp&r.mask) 62 | } 63 | } 64 | 65 | // Set get a proto to write. 66 | func (r *Ring) Set() (proto *protocol.Proto, err error) { 67 | if r.wp-r.rp >= r.num { 68 | return nil, errors.ErrRingFull 69 | } 70 | proto = &r.data[r.wp&r.mask] 71 | return 72 | } 73 | 74 | // SetAdv incr write index. 75 | func (r *Ring) SetAdv() { 76 | r.wp++ 77 | if conf.Conf.Debug { 78 | log.Infof("ring wp: %d, idx: %d", r.wp, r.wp&r.mask) 79 | } 80 | } 81 | 82 | // Reset reset ring. 83 | func (r *Ring) Reset() { 84 | r.rp = 0 85 | r.wp = 0 86 | // prevent pad compiler optimization 87 | // r.pad = [40]byte{} 88 | } 89 | -------------------------------------------------------------------------------- /internal/comet/room.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Terry-Mao/goim/api/protocol" 7 | "github.com/Terry-Mao/goim/internal/comet/errors" 8 | ) 9 | 10 | // Room is a room and store channel room info. 11 | type Room struct { 12 | ID string 13 | rLock sync.RWMutex 14 | next *Channel 15 | drop bool 16 | Online int32 // dirty read is ok 17 | AllOnline int32 18 | } 19 | 20 | // NewRoom new a room struct, store channel room info. 21 | func NewRoom(id string) (r *Room) { 22 | r = new(Room) 23 | r.ID = id 24 | r.drop = false 25 | r.next = nil 26 | r.Online = 0 27 | return 28 | } 29 | 30 | // Put put channel into the room. 31 | func (r *Room) Put(ch *Channel) (err error) { 32 | r.rLock.Lock() 33 | if !r.drop { 34 | if r.next != nil { 35 | r.next.Prev = ch 36 | } 37 | ch.Next = r.next 38 | ch.Prev = nil 39 | r.next = ch // insert to header 40 | r.Online++ 41 | } else { 42 | err = errors.ErrRoomDroped 43 | } 44 | r.rLock.Unlock() 45 | return 46 | } 47 | 48 | // Del delete channel from the room. 49 | func (r *Room) Del(ch *Channel) bool { 50 | r.rLock.Lock() 51 | if ch.Next != nil { 52 | // if not footer 53 | ch.Next.Prev = ch.Prev 54 | } 55 | if ch.Prev != nil { 56 | // if not header 57 | ch.Prev.Next = ch.Next 58 | } else { 59 | r.next = ch.Next 60 | } 61 | ch.Next = nil 62 | ch.Prev = nil 63 | r.Online-- 64 | r.drop = r.Online == 0 65 | r.rLock.Unlock() 66 | return r.drop 67 | } 68 | 69 | // Push push msg to the room, if chan full discard it. 70 | func (r *Room) Push(p *protocol.Proto) { 71 | r.rLock.RLock() 72 | for ch := r.next; ch != nil; ch = ch.Next { 73 | _ = ch.Push(p) 74 | } 75 | r.rLock.RUnlock() 76 | } 77 | 78 | // Close close the room. 79 | func (r *Room) Close() { 80 | r.rLock.RLock() 81 | for ch := r.next; ch != nil; ch = ch.Next { 82 | ch.Close() 83 | } 84 | r.rLock.RUnlock() 85 | } 86 | 87 | // OnlineNum the room all online. 88 | func (r *Room) OnlineNum() int32 { 89 | if r.AllOnline > 0 { 90 | return r.AllOnline 91 | } 92 | return r.Online 93 | } 94 | -------------------------------------------------------------------------------- /internal/comet/round.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "github.com/Terry-Mao/goim/internal/comet/conf" 5 | "github.com/Terry-Mao/goim/pkg/bytes" 6 | "github.com/Terry-Mao/goim/pkg/time" 7 | ) 8 | 9 | // RoundOptions round options. 10 | type RoundOptions struct { 11 | Timer int 12 | TimerSize int 13 | Reader int 14 | ReadBuf int 15 | ReadBufSize int 16 | Writer int 17 | WriteBuf int 18 | WriteBufSize int 19 | } 20 | 21 | // Round used for connection round-robin get a reader/writer/timer for split big lock. 22 | type Round struct { 23 | readers []bytes.Pool 24 | writers []bytes.Pool 25 | timers []time.Timer 26 | options RoundOptions 27 | } 28 | 29 | // NewRound new a round struct. 30 | func NewRound(c *conf.Config) (r *Round) { 31 | var i int 32 | r = &Round{ 33 | options: RoundOptions{ 34 | Reader: c.TCP.Reader, 35 | ReadBuf: c.TCP.ReadBuf, 36 | ReadBufSize: c.TCP.ReadBufSize, 37 | Writer: c.TCP.Writer, 38 | WriteBuf: c.TCP.WriteBuf, 39 | WriteBufSize: c.TCP.WriteBufSize, 40 | Timer: c.Protocol.Timer, 41 | TimerSize: c.Protocol.TimerSize, 42 | }} 43 | // reader 44 | r.readers = make([]bytes.Pool, r.options.Reader) 45 | for i = 0; i < r.options.Reader; i++ { 46 | r.readers[i].Init(r.options.ReadBuf, r.options.ReadBufSize) 47 | } 48 | // writer 49 | r.writers = make([]bytes.Pool, r.options.Writer) 50 | for i = 0; i < r.options.Writer; i++ { 51 | r.writers[i].Init(r.options.WriteBuf, r.options.WriteBufSize) 52 | } 53 | // timer 54 | r.timers = make([]time.Timer, r.options.Timer) 55 | for i = 0; i < r.options.Timer; i++ { 56 | r.timers[i].Init(r.options.TimerSize) 57 | } 58 | return 59 | } 60 | 61 | // Timer get a timer. 62 | func (r *Round) Timer(rn int) *time.Timer { 63 | return &(r.timers[rn%r.options.Timer]) 64 | } 65 | 66 | // Reader get a reader memory buffer. 67 | func (r *Round) Reader(rn int) *bytes.Pool { 68 | return &(r.readers[rn%r.options.Reader]) 69 | } 70 | 71 | // Writer get a writer memory buffer pool. 72 | func (r *Round) Writer(rn int) *bytes.Pool { 73 | return &(r.writers[rn%r.options.Writer]) 74 | } 75 | -------------------------------------------------------------------------------- /internal/comet/server.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/Terry-Mao/goim/api/logic" 9 | "github.com/Terry-Mao/goim/internal/comet/conf" 10 | log "github.com/golang/glog" 11 | "github.com/zhenjl/cityhash" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/balancer/roundrobin" 14 | "google.golang.org/grpc/keepalive" 15 | ) 16 | 17 | const ( 18 | minServerHeartbeat = time.Minute * 10 19 | maxServerHeartbeat = time.Minute * 30 20 | // grpc options 21 | grpcInitialWindowSize = 1 << 24 22 | grpcInitialConnWindowSize = 1 << 24 23 | grpcMaxSendMsgSize = 1 << 24 24 | grpcMaxCallMsgSize = 1 << 24 25 | grpcKeepAliveTime = time.Second * 10 26 | grpcKeepAliveTimeout = time.Second * 3 27 | grpcBackoffMaxDelay = time.Second * 3 28 | ) 29 | 30 | func newLogicClient(c *conf.RPCClient) logic.LogicClient { 31 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.Dial)) 32 | defer cancel() 33 | conn, err := grpc.DialContext(ctx, "discovery://default/goim.logic", 34 | []grpc.DialOption{ 35 | grpc.WithInsecure(), 36 | grpc.WithInitialWindowSize(grpcInitialWindowSize), 37 | grpc.WithInitialConnWindowSize(grpcInitialConnWindowSize), 38 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(grpcMaxCallMsgSize)), 39 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(grpcMaxSendMsgSize)), 40 | grpc.WithBackoffMaxDelay(grpcBackoffMaxDelay), 41 | grpc.WithKeepaliveParams(keepalive.ClientParameters{ 42 | Time: grpcKeepAliveTime, 43 | Timeout: grpcKeepAliveTimeout, 44 | PermitWithoutStream: true, 45 | }), 46 | grpc.WithBalancerName(roundrobin.Name), 47 | }...) 48 | if err != nil { 49 | panic(err) 50 | } 51 | return logic.NewLogicClient(conn) 52 | } 53 | 54 | // Server is comet server. 55 | type Server struct { 56 | c *conf.Config 57 | round *Round // accept round store 58 | buckets []*Bucket // subkey bucket 59 | bucketIdx uint32 60 | 61 | serverID string 62 | rpcClient logic.LogicClient 63 | } 64 | 65 | // NewServer returns a new Server. 66 | func NewServer(c *conf.Config) *Server { 67 | s := &Server{ 68 | c: c, 69 | round: NewRound(c), 70 | rpcClient: newLogicClient(c.RPCClient), 71 | } 72 | // init bucket 73 | s.buckets = make([]*Bucket, c.Bucket.Size) 74 | s.bucketIdx = uint32(c.Bucket.Size) 75 | for i := 0; i < c.Bucket.Size; i++ { 76 | s.buckets[i] = NewBucket(c.Bucket) 77 | } 78 | s.serverID = c.Env.Host 79 | go s.onlineproc() 80 | return s 81 | } 82 | 83 | // Buckets return all buckets. 84 | func (s *Server) Buckets() []*Bucket { 85 | return s.buckets 86 | } 87 | 88 | // Bucket get the bucket by subkey. 89 | func (s *Server) Bucket(subKey string) *Bucket { 90 | idx := cityhash.CityHash32([]byte(subKey), uint32(len(subKey))) % s.bucketIdx 91 | if conf.Conf.Debug { 92 | log.Infof("%s hit channel bucket index: %d use cityhash", subKey, idx) 93 | } 94 | return s.buckets[idx] 95 | } 96 | 97 | // RandServerHearbeat rand server heartbeat. 98 | func (s *Server) RandServerHearbeat() time.Duration { 99 | return (minServerHeartbeat + time.Duration(rand.Int63n(int64(maxServerHeartbeat-minServerHeartbeat)))) 100 | } 101 | 102 | // Close close the server. 103 | func (s *Server) Close() (err error) { 104 | return 105 | } 106 | 107 | func (s *Server) onlineproc() { 108 | for { 109 | var ( 110 | allRoomsCount map[string]int32 111 | err error 112 | ) 113 | roomCount := make(map[string]int32) 114 | for _, bucket := range s.buckets { 115 | for roomID, count := range bucket.RoomsCount() { 116 | roomCount[roomID] += count 117 | } 118 | } 119 | if allRoomsCount, err = s.RenewOnline(context.Background(), s.serverID, roomCount); err != nil { 120 | time.Sleep(time.Second) 121 | continue 122 | } 123 | for _, bucket := range s.buckets { 124 | bucket.UpRoomsCount(allRoomsCount) 125 | } 126 | time.Sleep(time.Second * 10) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /internal/comet/whitelist.go: -------------------------------------------------------------------------------- 1 | package comet 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/Terry-Mao/goim/internal/comet/conf" 8 | ) 9 | 10 | var whitelist *Whitelist 11 | 12 | // Whitelist . 13 | type Whitelist struct { 14 | log *log.Logger 15 | list map[int64]struct{} // whitelist for debug 16 | } 17 | 18 | // InitWhitelist a whitelist struct. 19 | func InitWhitelist(c *conf.Whitelist) (err error) { 20 | var ( 21 | mid int64 22 | f *os.File 23 | ) 24 | if f, err = os.OpenFile(c.WhiteLog, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644); err == nil { 25 | whitelist = new(Whitelist) 26 | whitelist.log = log.New(f, "", log.LstdFlags) 27 | whitelist.list = make(map[int64]struct{}) 28 | for _, mid = range c.Whitelist { 29 | whitelist.list[mid] = struct{}{} 30 | } 31 | } 32 | return 33 | } 34 | 35 | // Contains whitelist contains a mid or not. 36 | func (w *Whitelist) Contains(mid int64) (ok bool) { 37 | if mid > 0 { 38 | _, ok = w.list[mid] 39 | } 40 | return 41 | } 42 | 43 | // Printf calls l.Output to print to the logger. 44 | func (w *Whitelist) Printf(format string, v ...interface{}) { 45 | w.log.Printf(format, v...) 46 | } 47 | -------------------------------------------------------------------------------- /internal/job/comet.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/Terry-Mao/goim/api/comet" 11 | "github.com/Terry-Mao/goim/internal/job/conf" 12 | "github.com/bilibili/discovery/naming" 13 | 14 | log "github.com/golang/glog" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/keepalive" 17 | ) 18 | 19 | var ( 20 | // grpc options 21 | grpcKeepAliveTime = time.Duration(10) * time.Second 22 | grpcKeepAliveTimeout = time.Duration(3) * time.Second 23 | grpcBackoffMaxDelay = time.Duration(3) * time.Second 24 | grpcMaxSendMsgSize = 1 << 24 25 | grpcMaxCallMsgSize = 1 << 24 26 | ) 27 | 28 | const ( 29 | // grpc options 30 | grpcInitialWindowSize = 1 << 24 31 | grpcInitialConnWindowSize = 1 << 24 32 | ) 33 | 34 | func newCometClient(addr string) (comet.CometClient, error) { 35 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second)) 36 | defer cancel() 37 | conn, err := grpc.DialContext(ctx, addr, 38 | []grpc.DialOption{ 39 | grpc.WithInsecure(), 40 | grpc.WithInitialWindowSize(grpcInitialWindowSize), 41 | grpc.WithInitialConnWindowSize(grpcInitialConnWindowSize), 42 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(grpcMaxCallMsgSize)), 43 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(grpcMaxSendMsgSize)), 44 | grpc.WithBackoffMaxDelay(grpcBackoffMaxDelay), 45 | grpc.WithKeepaliveParams(keepalive.ClientParameters{ 46 | Time: grpcKeepAliveTime, 47 | Timeout: grpcKeepAliveTimeout, 48 | PermitWithoutStream: true, 49 | }), 50 | }..., 51 | ) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return comet.NewCometClient(conn), err 56 | } 57 | 58 | // Comet is a comet. 59 | type Comet struct { 60 | serverID string 61 | client comet.CometClient 62 | pushChan []chan *comet.PushMsgReq 63 | roomChan []chan *comet.BroadcastRoomReq 64 | broadcastChan chan *comet.BroadcastReq 65 | pushChanNum uint64 66 | roomChanNum uint64 67 | routineSize uint64 68 | 69 | ctx context.Context 70 | cancel context.CancelFunc 71 | } 72 | 73 | // NewComet new a comet. 74 | func NewComet(in *naming.Instance, c *conf.Comet) (*Comet, error) { 75 | cmt := &Comet{ 76 | serverID: in.Hostname, 77 | pushChan: make([]chan *comet.PushMsgReq, c.RoutineSize), 78 | roomChan: make([]chan *comet.BroadcastRoomReq, c.RoutineSize), 79 | broadcastChan: make(chan *comet.BroadcastReq, c.RoutineSize), 80 | routineSize: uint64(c.RoutineSize), 81 | } 82 | var grpcAddr string 83 | for _, addrs := range in.Addrs { 84 | u, err := url.Parse(addrs) 85 | if err == nil && u.Scheme == "grpc" { 86 | grpcAddr = u.Host 87 | } 88 | } 89 | if grpcAddr == "" { 90 | return nil, fmt.Errorf("invalid grpc address:%v", in.Addrs) 91 | } 92 | var err error 93 | if cmt.client, err = newCometClient(grpcAddr); err != nil { 94 | return nil, err 95 | } 96 | cmt.ctx, cmt.cancel = context.WithCancel(context.Background()) 97 | 98 | for i := 0; i < c.RoutineSize; i++ { 99 | cmt.pushChan[i] = make(chan *comet.PushMsgReq, c.RoutineChan) 100 | cmt.roomChan[i] = make(chan *comet.BroadcastRoomReq, c.RoutineChan) 101 | go cmt.process(cmt.pushChan[i], cmt.roomChan[i], cmt.broadcastChan) 102 | } 103 | return cmt, nil 104 | } 105 | 106 | // Push push a user message. 107 | func (c *Comet) Push(arg *comet.PushMsgReq) (err error) { 108 | idx := atomic.AddUint64(&c.pushChanNum, 1) % c.routineSize 109 | c.pushChan[idx] <- arg 110 | return 111 | } 112 | 113 | // BroadcastRoom broadcast a room message. 114 | func (c *Comet) BroadcastRoom(arg *comet.BroadcastRoomReq) (err error) { 115 | idx := atomic.AddUint64(&c.roomChanNum, 1) % c.routineSize 116 | c.roomChan[idx] <- arg 117 | return 118 | } 119 | 120 | // Broadcast broadcast a message. 121 | func (c *Comet) Broadcast(arg *comet.BroadcastReq) (err error) { 122 | c.broadcastChan <- arg 123 | return 124 | } 125 | 126 | func (c *Comet) process(pushChan chan *comet.PushMsgReq, roomChan chan *comet.BroadcastRoomReq, broadcastChan chan *comet.BroadcastReq) { 127 | for { 128 | select { 129 | case broadcastArg := <-broadcastChan: 130 | _, err := c.client.Broadcast(context.Background(), &comet.BroadcastReq{ 131 | Proto: broadcastArg.Proto, 132 | ProtoOp: broadcastArg.ProtoOp, 133 | Speed: broadcastArg.Speed, 134 | }) 135 | if err != nil { 136 | log.Errorf("c.client.Broadcast(%s, reply) serverId:%s error(%v)", broadcastArg, c.serverID, err) 137 | } 138 | case roomArg := <-roomChan: 139 | _, err := c.client.BroadcastRoom(context.Background(), &comet.BroadcastRoomReq{ 140 | RoomID: roomArg.RoomID, 141 | Proto: roomArg.Proto, 142 | }) 143 | if err != nil { 144 | log.Errorf("c.client.BroadcastRoom(%s, reply) serverId:%s error(%v)", roomArg, c.serverID, err) 145 | } 146 | case pushArg := <-pushChan: 147 | _, err := c.client.PushMsg(context.Background(), &comet.PushMsgReq{ 148 | Keys: pushArg.Keys, 149 | Proto: pushArg.Proto, 150 | ProtoOp: pushArg.ProtoOp, 151 | }) 152 | if err != nil { 153 | log.Errorf("c.client.PushMsg(%s, reply) serverId:%s error(%v)", pushArg, c.serverID, err) 154 | } 155 | case <-c.ctx.Done(): 156 | return 157 | } 158 | } 159 | } 160 | 161 | // Close close the resources. 162 | func (c *Comet) Close() (err error) { 163 | finish := make(chan bool) 164 | go func() { 165 | for { 166 | n := len(c.broadcastChan) 167 | for _, ch := range c.pushChan { 168 | n += len(ch) 169 | } 170 | for _, ch := range c.roomChan { 171 | n += len(ch) 172 | } 173 | if n == 0 { 174 | finish <- true 175 | return 176 | } 177 | time.Sleep(time.Second) 178 | } 179 | }() 180 | select { 181 | case <-finish: 182 | log.Info("close comet finish") 183 | case <-time.After(5 * time.Second): 184 | err = fmt.Errorf("close comet(server:%s push:%d room:%d broadcast:%d) timeout", c.serverID, len(c.pushChan), len(c.roomChan), len(c.broadcastChan)) 185 | } 186 | c.cancel() 187 | return 188 | } 189 | -------------------------------------------------------------------------------- /internal/job/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "time" 7 | 8 | "github.com/bilibili/discovery/naming" 9 | "github.com/BurntSushi/toml" 10 | xtime "github.com/Terry-Mao/goim/pkg/time" 11 | ) 12 | 13 | var ( 14 | confPath string 15 | region string 16 | zone string 17 | deployEnv string 18 | host string 19 | // Conf config 20 | Conf *Config 21 | ) 22 | 23 | func init() { 24 | var ( 25 | defHost, _ = os.Hostname() 26 | ) 27 | flag.StringVar(&confPath, "conf", "job-example.toml", "default config path") 28 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.") 29 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.") 30 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.") 31 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.") 32 | } 33 | 34 | // Init init config. 35 | func Init() (err error) { 36 | Conf = Default() 37 | _, err = toml.DecodeFile(confPath, &Conf) 38 | return 39 | } 40 | 41 | // Default new a config with specified defualt value. 42 | func Default() *Config { 43 | return &Config{ 44 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host}, 45 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host}, 46 | Comet: &Comet{RoutineChan: 1024, RoutineSize: 32}, 47 | Room: &Room{ 48 | Batch: 20, 49 | Signal: xtime.Duration(time.Second), 50 | Idle: xtime.Duration(time.Minute * 15), 51 | }, 52 | } 53 | } 54 | 55 | // Config is job config. 56 | type Config struct { 57 | Env *Env 58 | Kafka *Kafka 59 | Discovery *naming.Config 60 | Comet *Comet 61 | Room *Room 62 | } 63 | 64 | // Room is room config. 65 | type Room struct { 66 | Batch int 67 | Signal xtime.Duration 68 | Idle xtime.Duration 69 | } 70 | 71 | // Comet is comet config. 72 | type Comet struct { 73 | RoutineChan int 74 | RoutineSize int 75 | } 76 | 77 | // Kafka is kafka config. 78 | type Kafka struct { 79 | Topic string 80 | Group string 81 | Brokers []string 82 | } 83 | 84 | // Env is env config. 85 | type Env struct { 86 | Region string 87 | Zone string 88 | DeployEnv string 89 | Host string 90 | } 91 | -------------------------------------------------------------------------------- /internal/job/job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | pb "github.com/Terry-Mao/goim/api/logic" 10 | "github.com/Terry-Mao/goim/internal/job/conf" 11 | "github.com/bilibili/discovery/naming" 12 | "github.com/golang/protobuf/proto" 13 | 14 | cluster "github.com/bsm/sarama-cluster" 15 | log "github.com/golang/glog" 16 | ) 17 | 18 | // Job is push job. 19 | type Job struct { 20 | c *conf.Config 21 | consumer *cluster.Consumer 22 | cometServers map[string]*Comet 23 | 24 | rooms map[string]*Room 25 | roomsMutex sync.RWMutex 26 | } 27 | 28 | // New new a push job. 29 | func New(c *conf.Config) *Job { 30 | j := &Job{ 31 | c: c, 32 | consumer: newKafkaSub(c.Kafka), 33 | rooms: make(map[string]*Room), 34 | } 35 | j.watchComet(c.Discovery) 36 | return j 37 | } 38 | 39 | func newKafkaSub(c *conf.Kafka) *cluster.Consumer { 40 | config := cluster.NewConfig() 41 | config.Consumer.Return.Errors = true 42 | config.Group.Return.Notifications = true 43 | consumer, err := cluster.NewConsumer(c.Brokers, c.Group, []string{c.Topic}, config) 44 | if err != nil { 45 | panic(err) 46 | } 47 | return consumer 48 | } 49 | 50 | // Close close resounces. 51 | func (j *Job) Close() error { 52 | if j.consumer != nil { 53 | return j.consumer.Close() 54 | } 55 | return nil 56 | } 57 | 58 | // Consume messages, watch signals 59 | func (j *Job) Consume() { 60 | for { 61 | select { 62 | case err := <-j.consumer.Errors(): 63 | log.Errorf("consumer error(%v)", err) 64 | case n := <-j.consumer.Notifications(): 65 | log.Infof("consumer rebalanced(%v)", n) 66 | case msg, ok := <-j.consumer.Messages(): 67 | if !ok { 68 | return 69 | } 70 | j.consumer.MarkOffset(msg, "") 71 | // process push message 72 | pushMsg := new(pb.PushMsg) 73 | if err := proto.Unmarshal(msg.Value, pushMsg); err != nil { 74 | log.Errorf("proto.Unmarshal(%v) error(%v)", msg, err) 75 | continue 76 | } 77 | if err := j.push(context.Background(), pushMsg); err != nil { 78 | log.Errorf("j.push(%v) error(%v)", pushMsg, err) 79 | } 80 | log.Infof("consume: %s/%d/%d\t%s\t%+v", msg.Topic, msg.Partition, msg.Offset, msg.Key, pushMsg) 81 | } 82 | } 83 | } 84 | 85 | func (j *Job) watchComet(c *naming.Config) { 86 | dis := naming.New(c) 87 | resolver := dis.Build("goim.comet") 88 | event := resolver.Watch() 89 | select { 90 | case _, ok := <-event: 91 | if !ok { 92 | panic("watchComet init failed") 93 | } 94 | if ins, ok := resolver.Fetch(); ok { 95 | if err := j.newAddress(ins.Instances); err != nil { 96 | panic(err) 97 | } 98 | log.Infof("watchComet init newAddress:%+v", ins) 99 | } 100 | case <-time.After(10 * time.Second): 101 | log.Error("watchComet init instances timeout") 102 | } 103 | go func() { 104 | for { 105 | if _, ok := <-event; !ok { 106 | log.Info("watchComet exit") 107 | return 108 | } 109 | ins, ok := resolver.Fetch() 110 | if ok { 111 | if err := j.newAddress(ins.Instances); err != nil { 112 | log.Errorf("watchComet newAddress(%+v) error(%+v)", ins, err) 113 | continue 114 | } 115 | log.Infof("watchComet change newAddress:%+v", ins) 116 | } 117 | } 118 | }() 119 | } 120 | 121 | func (j *Job) newAddress(insMap map[string][]*naming.Instance) error { 122 | ins := insMap[j.c.Env.Zone] 123 | if len(ins) == 0 { 124 | return fmt.Errorf("watchComet instance is empty") 125 | } 126 | comets := map[string]*Comet{} 127 | for _, in := range ins { 128 | if old, ok := j.cometServers[in.Hostname]; ok { 129 | comets[in.Hostname] = old 130 | continue 131 | } 132 | c, err := NewComet(in, j.c.Comet) 133 | if err != nil { 134 | log.Errorf("watchComet NewComet(%+v) error(%v)", in, err) 135 | return err 136 | } 137 | comets[in.Hostname] = c 138 | log.Infof("watchComet AddComet grpc:%+v", in) 139 | } 140 | for key, old := range j.cometServers { 141 | if _, ok := comets[key]; !ok { 142 | old.cancel() 143 | log.Infof("watchComet DelComet:%s", key) 144 | } 145 | } 146 | j.cometServers = comets 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/job/push.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Terry-Mao/goim/api/comet" 8 | pb "github.com/Terry-Mao/goim/api/logic" 9 | "github.com/Terry-Mao/goim/api/protocol" 10 | "github.com/Terry-Mao/goim/pkg/bytes" 11 | log "github.com/golang/glog" 12 | ) 13 | 14 | func (j *Job) push(ctx context.Context, pushMsg *pb.PushMsg) (err error) { 15 | switch pushMsg.Type { 16 | case pb.PushMsg_PUSH: 17 | err = j.pushKeys(pushMsg.Operation, pushMsg.Server, pushMsg.Keys, pushMsg.Msg) 18 | case pb.PushMsg_ROOM: 19 | err = j.getRoom(pushMsg.Room).Push(pushMsg.Operation, pushMsg.Msg) 20 | case pb.PushMsg_BROADCAST: 21 | err = j.broadcast(pushMsg.Operation, pushMsg.Msg, pushMsg.Speed) 22 | default: 23 | err = fmt.Errorf("no match push type: %s", pushMsg.Type) 24 | } 25 | return 26 | } 27 | 28 | // pushKeys push a message to a batch of subkeys. 29 | func (j *Job) pushKeys(operation int32, serverID string, subKeys []string, body []byte) (err error) { 30 | buf := bytes.NewWriterSize(len(body) + 64) 31 | p := &protocol.Proto{ 32 | Ver: 1, 33 | Op: operation, 34 | Body: body, 35 | } 36 | p.WriteTo(buf) 37 | p.Body = buf.Buffer() 38 | p.Op = protocol.OpRaw 39 | var args = comet.PushMsgReq{ 40 | Keys: subKeys, 41 | ProtoOp: operation, 42 | Proto: p, 43 | } 44 | if c, ok := j.cometServers[serverID]; ok { 45 | if err = c.Push(&args); err != nil { 46 | log.Errorf("c.Push(%v) serverID:%s error(%v)", args, serverID, err) 47 | } 48 | log.Infof("pushKey:%s comets:%d", serverID, len(j.cometServers)) 49 | } 50 | return 51 | } 52 | 53 | // broadcast broadcast a message to all. 54 | func (j *Job) broadcast(operation int32, body []byte, speed int32) (err error) { 55 | buf := bytes.NewWriterSize(len(body) + 64) 56 | p := &protocol.Proto{ 57 | Ver: 1, 58 | Op: operation, 59 | Body: body, 60 | } 61 | p.WriteTo(buf) 62 | p.Body = buf.Buffer() 63 | p.Op = protocol.OpRaw 64 | comets := j.cometServers 65 | speed /= int32(len(comets)) 66 | var args = comet.BroadcastReq{ 67 | ProtoOp: operation, 68 | Proto: p, 69 | Speed: speed, 70 | } 71 | for serverID, c := range comets { 72 | if err = c.Broadcast(&args); err != nil { 73 | log.Errorf("c.Broadcast(%v) serverID:%s error(%v)", args, serverID, err) 74 | } 75 | } 76 | log.Infof("broadcast comets:%d", len(comets)) 77 | return 78 | } 79 | 80 | // broadcastRoomRawBytes broadcast aggregation messages to room. 81 | func (j *Job) broadcastRoomRawBytes(roomID string, body []byte) (err error) { 82 | args := comet.BroadcastRoomReq{ 83 | RoomID: roomID, 84 | Proto: &protocol.Proto{ 85 | Ver: 1, 86 | Op: protocol.OpRaw, 87 | Body: body, 88 | }, 89 | } 90 | comets := j.cometServers 91 | for serverID, c := range comets { 92 | if err = c.BroadcastRoom(&args); err != nil { 93 | log.Errorf("c.BroadcastRoom(%v) roomID:%s serverID:%s error(%v)", args, roomID, serverID, err) 94 | } 95 | } 96 | log.Infof("broadcastRoom comets:%d", len(comets)) 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /internal/job/room.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/Terry-Mao/goim/api/protocol" 8 | "github.com/Terry-Mao/goim/internal/job/conf" 9 | "github.com/Terry-Mao/goim/pkg/bytes" 10 | log "github.com/golang/glog" 11 | ) 12 | 13 | var ( 14 | // ErrComet commet error. 15 | ErrComet = errors.New("comet rpc is not available") 16 | // ErrCometFull comet chan full. 17 | ErrCometFull = errors.New("comet proto chan full") 18 | // ErrRoomFull room chan full. 19 | ErrRoomFull = errors.New("room proto chan full") 20 | 21 | roomReadyProto = new(protocol.Proto) 22 | ) 23 | 24 | // Room room. 25 | type Room struct { 26 | c *conf.Room 27 | job *Job 28 | id string 29 | proto chan *protocol.Proto 30 | } 31 | 32 | // NewRoom new a room struct, store channel room info. 33 | func NewRoom(job *Job, id string, c *conf.Room) (r *Room) { 34 | r = &Room{ 35 | c: c, 36 | id: id, 37 | job: job, 38 | proto: make(chan *protocol.Proto, c.Batch*2), 39 | } 40 | go r.pushproc(c.Batch, time.Duration(c.Signal)) 41 | return 42 | } 43 | 44 | // Push push msg to the room, if chan full discard it. 45 | func (r *Room) Push(op int32, msg []byte) (err error) { 46 | var p = &protocol.Proto{ 47 | Ver: 1, 48 | Op: op, 49 | Body: msg, 50 | } 51 | select { 52 | case r.proto <- p: 53 | default: 54 | err = ErrRoomFull 55 | } 56 | return 57 | } 58 | 59 | // pushproc merge proto and push msgs in batch. 60 | func (r *Room) pushproc(batch int, sigTime time.Duration) { 61 | var ( 62 | n int 63 | last time.Time 64 | p *protocol.Proto 65 | buf = bytes.NewWriterSize(int(protocol.MaxBodySize)) 66 | ) 67 | log.Infof("start room:%s goroutine", r.id) 68 | td := time.AfterFunc(sigTime, func() { 69 | select { 70 | case r.proto <- roomReadyProto: 71 | default: 72 | } 73 | }) 74 | defer td.Stop() 75 | for { 76 | if p = <-r.proto; p == nil { 77 | break // exit 78 | } else if p != roomReadyProto { 79 | // merge buffer ignore error, always nil 80 | p.WriteTo(buf) 81 | if n++; n == 1 { 82 | last = time.Now() 83 | td.Reset(sigTime) 84 | continue 85 | } else if n < batch { 86 | if sigTime > time.Since(last) { 87 | continue 88 | } 89 | } 90 | } else { 91 | if n == 0 { 92 | break 93 | } 94 | } 95 | _ = r.job.broadcastRoomRawBytes(r.id, buf.Buffer()) 96 | // TODO use reset buffer 97 | // after push to room channel, renew a buffer, let old buffer gc 98 | buf = bytes.NewWriterSize(buf.Size()) 99 | n = 0 100 | if r.c.Idle != 0 { 101 | td.Reset(time.Duration(r.c.Idle)) 102 | } else { 103 | td.Reset(time.Minute) 104 | } 105 | } 106 | r.job.delRoom(r.id) 107 | log.Infof("room:%s goroutine exit", r.id) 108 | } 109 | 110 | func (j *Job) delRoom(roomID string) { 111 | j.roomsMutex.Lock() 112 | delete(j.rooms, roomID) 113 | j.roomsMutex.Unlock() 114 | } 115 | 116 | func (j *Job) getRoom(roomID string) *Room { 117 | j.roomsMutex.RLock() 118 | room, ok := j.rooms[roomID] 119 | j.roomsMutex.RUnlock() 120 | if !ok { 121 | j.roomsMutex.Lock() 122 | if room, ok = j.rooms[roomID]; !ok { 123 | room = NewRoom(j, roomID, j.c.Room) 124 | j.rooms[roomID] = room 125 | } 126 | j.roomsMutex.Unlock() 127 | log.Infof("new a room:%s active:%d", roomID, len(j.rooms)) 128 | } 129 | return room 130 | } 131 | -------------------------------------------------------------------------------- /internal/logic/balancer.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/bilibili/discovery/naming" 12 | "github.com/Terry-Mao/goim/internal/logic/model" 13 | log "github.com/golang/glog" 14 | ) 15 | 16 | const ( 17 | _minWeight = 1 18 | _maxWeight = 1 << 20 19 | _maxNodes = 5 20 | ) 21 | 22 | type weightedNode struct { 23 | region string 24 | hostname string 25 | addrs []string 26 | fixedWeight int64 27 | currentWeight int64 28 | currentConns int64 29 | updated int64 30 | } 31 | 32 | func (w *weightedNode) String() string { 33 | return fmt.Sprintf("region:%s fixedWeight:%d, currentWeight:%d, currentConns:%d", w.region, w.fixedWeight, w.currentWeight, w.currentConns) 34 | } 35 | 36 | func (w *weightedNode) chosen() { 37 | w.currentConns++ 38 | } 39 | 40 | func (w *weightedNode) reset() { 41 | w.currentWeight = 0 42 | } 43 | 44 | func (w *weightedNode) calculateWeight(totalWeight, totalConns int64, gainWeight float64) { 45 | fixedWeight := float64(w.fixedWeight) * gainWeight 46 | totalWeight += int64(fixedWeight) - w.fixedWeight 47 | if totalConns > 0 { 48 | weightRatio := fixedWeight / float64(totalWeight) 49 | var connRatio float64 50 | if totalConns != 0 { 51 | connRatio = float64(w.currentConns) / float64(totalConns) * 0.5 52 | } 53 | diff := weightRatio - connRatio 54 | multiple := diff * float64(totalConns) 55 | floor := math.Floor(multiple) 56 | if floor-multiple >= -0.5 { 57 | w.currentWeight = int64(fixedWeight + floor) 58 | } else { 59 | w.currentWeight = int64(fixedWeight + math.Ceil(multiple)) 60 | } 61 | if diff < 0 { 62 | // we always return the max from minWeight and calculated Current weight 63 | if _minWeight > w.currentWeight { 64 | w.currentWeight = _minWeight 65 | } 66 | } else { 67 | // we always return the min from maxWeight and calculated Current weight 68 | if _maxWeight < w.currentWeight { 69 | w.currentWeight = _maxWeight 70 | } 71 | } 72 | } else { 73 | w.reset() 74 | } 75 | } 76 | 77 | // LoadBalancer load balancer. 78 | type LoadBalancer struct { 79 | totalConns int64 80 | totalWeight int64 81 | nodes map[string]*weightedNode 82 | nodesMutex sync.Mutex 83 | } 84 | 85 | // NewLoadBalancer new a load balancer. 86 | func NewLoadBalancer() *LoadBalancer { 87 | lb := &LoadBalancer{ 88 | nodes: make(map[string]*weightedNode), 89 | } 90 | return lb 91 | } 92 | 93 | // Size return node size. 94 | func (lb *LoadBalancer) Size() int { 95 | return len(lb.nodes) 96 | } 97 | 98 | func (lb *LoadBalancer) weightedNodes(region string, regionWeight float64) (nodes []*weightedNode) { 99 | for _, n := range lb.nodes { 100 | var gainWeight = float64(1.0) 101 | if n.region == region { 102 | gainWeight *= regionWeight 103 | } 104 | n.calculateWeight(lb.totalWeight, lb.totalConns, gainWeight) 105 | nodes = append(nodes, n) 106 | } 107 | sort.Slice(nodes, func(i, j int) bool { 108 | return nodes[i].currentWeight > nodes[j].currentWeight 109 | }) 110 | if len(nodes) > 0 { 111 | nodes[0].chosen() 112 | lb.totalConns++ 113 | } 114 | return 115 | } 116 | 117 | // NodeAddrs return node addrs. 118 | func (lb *LoadBalancer) NodeAddrs(region, domain string, regionWeight float64) (domains, addrs []string) { 119 | lb.nodesMutex.Lock() 120 | nodes := lb.weightedNodes(region, regionWeight) 121 | lb.nodesMutex.Unlock() 122 | for i, n := range nodes { 123 | if i == _maxNodes { 124 | break 125 | } 126 | domains = append(domains, n.hostname+domain) 127 | addrs = append(addrs, n.addrs...) 128 | } 129 | return 130 | } 131 | 132 | // Update update server nodes. 133 | func (lb *LoadBalancer) Update(ins []*naming.Instance) { 134 | var ( 135 | totalConns int64 136 | totalWeight int64 137 | nodes = make(map[string]*weightedNode, len(ins)) 138 | ) 139 | if len(ins) == 0 || float32(len(ins))/float32(len(lb.nodes)) < 0.5 { 140 | log.Errorf("load balancer update src:%d target:%d less than half", len(lb.nodes), len(ins)) 141 | return 142 | } 143 | lb.nodesMutex.Lock() 144 | for _, in := range ins { 145 | if old, ok := lb.nodes[in.Hostname]; ok && old.updated == in.LastTs { 146 | nodes[in.Hostname] = old 147 | totalConns += old.currentConns 148 | totalWeight += old.fixedWeight 149 | } else { 150 | meta := in.Metadata 151 | weight, err := strconv.ParseInt(meta[model.MetaWeight], 10, 32) 152 | if err != nil { 153 | log.Errorf("instance(%+v) strconv.ParseInt(weight:%s) error(%v)", in, meta[model.MetaWeight], err) 154 | continue 155 | } 156 | conns, err := strconv.ParseInt(meta[model.MetaConnCount], 10, 32) 157 | if err != nil { 158 | log.Errorf("instance(%+v) strconv.ParseInt(conns:%s) error(%v)", in, meta[model.MetaConnCount], err) 159 | continue 160 | } 161 | nodes[in.Hostname] = &weightedNode{ 162 | region: in.Region, 163 | hostname: in.Hostname, 164 | fixedWeight: weight, 165 | currentConns: conns, 166 | addrs: strings.Split(meta[model.MetaAddrs], ","), 167 | updated: in.LastTs, 168 | } 169 | totalConns += conns 170 | totalWeight += weight 171 | } 172 | } 173 | lb.nodes = nodes 174 | lb.totalConns = totalConns 175 | lb.totalWeight = totalWeight 176 | lb.nodesMutex.Unlock() 177 | } 178 | -------------------------------------------------------------------------------- /internal/logic/balancer_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/bilibili/discovery/naming" 8 | "github.com/Terry-Mao/goim/internal/logic/model" 9 | ) 10 | 11 | func TestWeightedNode(t *testing.T) { 12 | nodes := []*weightedNode{ 13 | &weightedNode{fixedWeight: 1, currentWeight: 1, currentConns: 1000000}, 14 | &weightedNode{fixedWeight: 2, currentWeight: 1, currentConns: 1000000}, 15 | &weightedNode{fixedWeight: 3, currentWeight: 1, currentConns: 1000000}, 16 | } 17 | for i := 0; i < 100; i++ { 18 | for _, n := range nodes { 19 | n.calculateWeight(6, nodes[0].currentConns+nodes[1].currentConns+nodes[2].currentConns, 1.0) 20 | } 21 | sort.Slice(nodes, func(i, j int) bool { 22 | return nodes[i].currentWeight > nodes[j].currentWeight 23 | }) 24 | nodes[0].chosen() 25 | } 26 | ft := float64(nodes[0].fixedWeight + nodes[1].fixedWeight + nodes[2].fixedWeight) 27 | ct := float64(nodes[0].currentConns + nodes[1].currentConns + nodes[2].currentConns) 28 | for _, n := range nodes { 29 | t.Logf("match ratio %d:%d", int(float64(n.fixedWeight)/ft*100*0.6), int(float64(n.currentConns)/ct*100)) 30 | } 31 | } 32 | 33 | func TestLoadBalancer(t *testing.T) { 34 | ss := []*naming.Instance{ 35 | &naming.Instance{ 36 | Region: "bj", 37 | Hostname: "01", 38 | Metadata: map[string]string{ 39 | model.MetaWeight: "10", 40 | model.MetaConnCount: "240590", 41 | model.MetaIPCount: "10", 42 | model.MetaAddrs: "ip_bj", 43 | }, 44 | }, 45 | &naming.Instance{ 46 | Region: "sh", 47 | Hostname: "02", 48 | Metadata: map[string]string{ 49 | model.MetaWeight: "10", 50 | model.MetaConnCount: "375420", 51 | model.MetaIPCount: "10", 52 | model.MetaAddrs: "ip_sh", 53 | }, 54 | }, 55 | &naming.Instance{ 56 | Region: "gz", 57 | Hostname: "03", 58 | Metadata: map[string]string{ 59 | model.MetaWeight: "10", 60 | model.MetaConnCount: "293430", 61 | model.MetaIPCount: "10", 62 | model.MetaAddrs: "ip_gz", 63 | }, 64 | }, 65 | } 66 | lb := NewLoadBalancer() 67 | lb.Update(ss) 68 | for i := 0; i < 5; i++ { 69 | t.Log(lb.NodeAddrs("sh", ".test", 1.6)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/logic/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/bilibili/discovery/naming" 10 | xtime "github.com/Terry-Mao/goim/pkg/time" 11 | 12 | "github.com/BurntSushi/toml" 13 | ) 14 | 15 | var ( 16 | confPath string 17 | region string 18 | zone string 19 | deployEnv string 20 | host string 21 | weight int64 22 | 23 | // Conf config 24 | Conf *Config 25 | ) 26 | 27 | func init() { 28 | var ( 29 | defHost, _ = os.Hostname() 30 | defWeight, _ = strconv.ParseInt(os.Getenv("WEIGHT"), 10, 32) 31 | ) 32 | flag.StringVar(&confPath, "conf", "logic-example.toml", "default config path") 33 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.") 34 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.") 35 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.") 36 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.") 37 | flag.Int64Var(&weight, "weight", defWeight, "load balancing weight, or use WEIGHT env variable, value: 10 etc.") 38 | } 39 | 40 | // Init init config. 41 | func Init() (err error) { 42 | Conf = Default() 43 | _, err = toml.DecodeFile(confPath, &Conf) 44 | return 45 | } 46 | 47 | // Default new a config with specified defualt value. 48 | func Default() *Config { 49 | return &Config{ 50 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host, Weight: weight}, 51 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host}, 52 | HTTPServer: &HTTPServer{ 53 | Network: "tcp", 54 | Addr: "3111", 55 | ReadTimeout: xtime.Duration(time.Second), 56 | WriteTimeout: xtime.Duration(time.Second), 57 | }, 58 | RPCClient: &RPCClient{Dial: xtime.Duration(time.Second), Timeout: xtime.Duration(time.Second)}, 59 | RPCServer: &RPCServer{ 60 | Network: "tcp", 61 | Addr: "3119", 62 | Timeout: xtime.Duration(time.Second), 63 | IdleTimeout: xtime.Duration(time.Second * 60), 64 | MaxLifeTime: xtime.Duration(time.Hour * 2), 65 | ForceCloseWait: xtime.Duration(time.Second * 20), 66 | KeepAliveInterval: xtime.Duration(time.Second * 60), 67 | KeepAliveTimeout: xtime.Duration(time.Second * 20), 68 | }, 69 | Backoff: &Backoff{MaxDelay: 300, BaseDelay: 3, Factor: 1.8, Jitter: 1.3}, 70 | } 71 | } 72 | 73 | // Config config. 74 | type Config struct { 75 | Env *Env 76 | Discovery *naming.Config 77 | RPCClient *RPCClient 78 | RPCServer *RPCServer 79 | HTTPServer *HTTPServer 80 | Kafka *Kafka 81 | Redis *Redis 82 | Node *Node 83 | Backoff *Backoff 84 | Regions map[string][]string 85 | } 86 | 87 | // Env is env config. 88 | type Env struct { 89 | Region string 90 | Zone string 91 | DeployEnv string 92 | Host string 93 | Weight int64 94 | } 95 | 96 | // Node node config. 97 | type Node struct { 98 | DefaultDomain string 99 | HostDomain string 100 | TCPPort int 101 | WSPort int 102 | WSSPort int 103 | HeartbeatMax int 104 | Heartbeat xtime.Duration 105 | RegionWeight float64 106 | } 107 | 108 | // Backoff backoff. 109 | type Backoff struct { 110 | MaxDelay int32 111 | BaseDelay int32 112 | Factor float32 113 | Jitter float32 114 | } 115 | 116 | // Redis . 117 | type Redis struct { 118 | Network string 119 | Addr string 120 | Auth string 121 | Active int 122 | Idle int 123 | DialTimeout xtime.Duration 124 | ReadTimeout xtime.Duration 125 | WriteTimeout xtime.Duration 126 | IdleTimeout xtime.Duration 127 | Expire xtime.Duration 128 | } 129 | 130 | // Kafka . 131 | type Kafka struct { 132 | Topic string 133 | Brokers []string 134 | } 135 | 136 | // RPCClient is RPC client config. 137 | type RPCClient struct { 138 | Dial xtime.Duration 139 | Timeout xtime.Duration 140 | } 141 | 142 | // RPCServer is RPC server config. 143 | type RPCServer struct { 144 | Network string 145 | Addr string 146 | Timeout xtime.Duration 147 | IdleTimeout xtime.Duration 148 | MaxLifeTime xtime.Duration 149 | ForceCloseWait xtime.Duration 150 | KeepAliveInterval xtime.Duration 151 | KeepAliveTimeout xtime.Duration 152 | } 153 | 154 | // HTTPServer is http server config. 155 | type HTTPServer struct { 156 | Network string 157 | Addr string 158 | ReadTimeout xtime.Duration 159 | WriteTimeout xtime.Duration 160 | } 161 | -------------------------------------------------------------------------------- /internal/logic/conn.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/Terry-Mao/goim/api/protocol" 9 | "github.com/Terry-Mao/goim/internal/logic/model" 10 | log "github.com/golang/glog" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | // Connect connected a conn. 15 | func (l *Logic) Connect(c context.Context, server, cookie string, token []byte) (mid int64, key, roomID string, accepts []int32, hb int64, err error) { 16 | var params struct { 17 | Mid int64 `json:"mid"` 18 | Key string `json:"key"` 19 | RoomID string `json:"room_id"` 20 | Platform string `json:"platform"` 21 | Accepts []int32 `json:"accepts"` 22 | } 23 | if err = json.Unmarshal(token, ¶ms); err != nil { 24 | log.Errorf("json.Unmarshal(%s) error(%v)", token, err) 25 | return 26 | } 27 | mid = params.Mid 28 | roomID = params.RoomID 29 | accepts = params.Accepts 30 | hb = int64(l.c.Node.Heartbeat) * int64(l.c.Node.HeartbeatMax) 31 | if key = params.Key; key == "" { 32 | key = uuid.New().String() 33 | } 34 | if err = l.dao.AddMapping(c, mid, key, server); err != nil { 35 | log.Errorf("l.dao.AddMapping(%d,%s,%s) error(%v)", mid, key, server, err) 36 | } 37 | log.Infof("conn connected key:%s server:%s mid:%d token:%s", key, server, mid, token) 38 | return 39 | } 40 | 41 | // Disconnect disconnect a conn. 42 | func (l *Logic) Disconnect(c context.Context, mid int64, key, server string) (has bool, err error) { 43 | if has, err = l.dao.DelMapping(c, mid, key, server); err != nil { 44 | log.Errorf("l.dao.DelMapping(%d,%s) error(%v)", mid, key, server) 45 | return 46 | } 47 | log.Infof("conn disconnected key:%s server:%s mid:%d", key, server, mid) 48 | return 49 | } 50 | 51 | // Heartbeat heartbeat a conn. 52 | func (l *Logic) Heartbeat(c context.Context, mid int64, key, server string) (err error) { 53 | has, err := l.dao.ExpireMapping(c, mid, key) 54 | if err != nil { 55 | log.Errorf("l.dao.ExpireMapping(%d,%s,%s) error(%v)", mid, key, server, err) 56 | return 57 | } 58 | if !has { 59 | if err = l.dao.AddMapping(c, mid, key, server); err != nil { 60 | log.Errorf("l.dao.AddMapping(%d,%s,%s) error(%v)", mid, key, server, err) 61 | return 62 | } 63 | } 64 | log.Infof("conn heartbeat key:%s server:%s mid:%d", key, server, mid) 65 | return 66 | } 67 | 68 | // RenewOnline renew a server online. 69 | func (l *Logic) RenewOnline(c context.Context, server string, roomCount map[string]int32) (map[string]int32, error) { 70 | online := &model.Online{ 71 | Server: server, 72 | RoomCount: roomCount, 73 | Updated: time.Now().Unix(), 74 | } 75 | if err := l.dao.AddServerOnline(context.Background(), server, online); err != nil { 76 | return nil, err 77 | } 78 | return l.roomCount, nil 79 | } 80 | 81 | // Receive receive a message. 82 | func (l *Logic) Receive(c context.Context, mid int64, proto *protocol.Proto) (err error) { 83 | log.Infof("receive mid:%d message:%+v", mid, proto) 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /internal/logic/conn_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Terry-Mao/goim/api/protocol" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConnect(t *testing.T) { 12 | var ( 13 | server = "test_server" 14 | serverKey = "test_server_key" 15 | cookie = "" 16 | token = []byte(`{"mid":1, "key":"test_server_key", "room_id":"test://test_room", "platform":"web", "accepts":[1000,1001,1002]}`) 17 | ol = map[string]int32{"test://test_room": 100} 18 | c = context.Background() 19 | ) 20 | // connect 21 | mid, key, roomID, accepts, hb, err := lg.Connect(c, server, cookie, token) 22 | assert.Nil(t, err) 23 | assert.Equal(t, serverKey, key) 24 | assert.Equal(t, roomID, "test://test_room") 25 | assert.Equal(t, len(accepts), 3) 26 | assert.NotZero(t, hb) 27 | t.Log(mid, key, roomID, accepts, err) 28 | // heartbeat 29 | err = lg.Heartbeat(c, mid, key, server) 30 | assert.Nil(t, err) 31 | // disconnect 32 | has, err := lg.Disconnect(c, mid, key, server) 33 | assert.Nil(t, err) 34 | assert.Equal(t, true, has) 35 | // renew 36 | online, err := lg.RenewOnline(c, server, ol) 37 | assert.Nil(t, err) 38 | assert.NotNil(t, online) 39 | // message 40 | err = lg.Receive(c, mid, &protocol.Proto{}) 41 | assert.Nil(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /internal/logic/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/Terry-Mao/goim/internal/logic/conf" 8 | "github.com/gomodule/redigo/redis" 9 | kafka "gopkg.in/Shopify/sarama.v1" 10 | ) 11 | 12 | // Dao dao. 13 | type Dao struct { 14 | c *conf.Config 15 | kafkaPub kafka.SyncProducer 16 | redis *redis.Pool 17 | redisExpire int32 18 | } 19 | 20 | // New new a dao and return. 21 | func New(c *conf.Config) *Dao { 22 | d := &Dao{ 23 | c: c, 24 | kafkaPub: newKafkaPub(c.Kafka), 25 | redis: newRedis(c.Redis), 26 | redisExpire: int32(time.Duration(c.Redis.Expire) / time.Second), 27 | } 28 | return d 29 | } 30 | 31 | func newKafkaPub(c *conf.Kafka) kafka.SyncProducer { 32 | kc := kafka.NewConfig() 33 | kc.Producer.RequiredAcks = kafka.WaitForAll // Wait for all in-sync replicas to ack the message 34 | kc.Producer.Retry.Max = 10 // Retry up to 10 times to produce the message 35 | kc.Producer.Return.Successes = true 36 | pub, err := kafka.NewSyncProducer(c.Brokers, kc) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return pub 41 | } 42 | 43 | func newRedis(c *conf.Redis) *redis.Pool { 44 | return &redis.Pool{ 45 | MaxIdle: c.Idle, 46 | MaxActive: c.Active, 47 | IdleTimeout: time.Duration(c.IdleTimeout), 48 | Dial: func() (redis.Conn, error) { 49 | conn, err := redis.Dial(c.Network, c.Addr, 50 | redis.DialConnectTimeout(time.Duration(c.DialTimeout)), 51 | redis.DialReadTimeout(time.Duration(c.ReadTimeout)), 52 | redis.DialWriteTimeout(time.Duration(c.WriteTimeout)), 53 | redis.DialPassword(c.Auth), 54 | ) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return conn, nil 59 | }, 60 | } 61 | } 62 | 63 | // Close close the resource. 64 | func (d *Dao) Close() error { 65 | return d.redis.Close() 66 | } 67 | 68 | // Ping dao ping. 69 | func (d *Dao) Ping(c context.Context) error { 70 | return d.pingRedis(c) 71 | } 72 | -------------------------------------------------------------------------------- /internal/logic/dao/dao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "os" 7 | "testing" 8 | 9 | "github.com/Terry-Mao/goim/internal/logic/conf" 10 | ) 11 | 12 | var ( 13 | d *Dao 14 | ) 15 | 16 | func TestMain(m *testing.M) { 17 | if err := flag.Set("conf", "../../../cmd/logic/logic-example.toml"); err != nil { 18 | panic(err) 19 | } 20 | flag.Parse() 21 | if err := conf.Init(); err != nil { 22 | panic(err) 23 | } 24 | d = New(conf.Conf) 25 | if err := d.Ping(context.TODO()); err != nil { 26 | os.Exit(-1) 27 | } 28 | if err := d.Close(); err != nil { 29 | os.Exit(-1) 30 | } 31 | if err := d.Ping(context.TODO()); err == nil { 32 | os.Exit(-1) 33 | } 34 | d = New(conf.Conf) 35 | os.Exit(m.Run()) 36 | } 37 | -------------------------------------------------------------------------------- /internal/logic/dao/kafka.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | pb "github.com/Terry-Mao/goim/api/logic" 8 | log "github.com/golang/glog" 9 | "github.com/golang/protobuf/proto" 10 | sarama "gopkg.in/Shopify/sarama.v1" 11 | ) 12 | 13 | // PushMsg push a message to databus. 14 | func (d *Dao) PushMsg(c context.Context, op int32, server string, keys []string, msg []byte) (err error) { 15 | pushMsg := &pb.PushMsg{ 16 | Type: pb.PushMsg_PUSH, 17 | Operation: op, 18 | Server: server, 19 | Keys: keys, 20 | Msg: msg, 21 | } 22 | b, err := proto.Marshal(pushMsg) 23 | if err != nil { 24 | return 25 | } 26 | m := &sarama.ProducerMessage{ 27 | Key: sarama.StringEncoder(keys[0]), 28 | Topic: d.c.Kafka.Topic, 29 | Value: sarama.ByteEncoder(b), 30 | } 31 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil { 32 | log.Errorf("PushMsg.send(push pushMsg:%v) error(%v)", pushMsg, err) 33 | } 34 | return 35 | } 36 | 37 | // BroadcastRoomMsg push a message to databus. 38 | func (d *Dao) BroadcastRoomMsg(c context.Context, op int32, room string, msg []byte) (err error) { 39 | pushMsg := &pb.PushMsg{ 40 | Type: pb.PushMsg_ROOM, 41 | Operation: op, 42 | Room: room, 43 | Msg: msg, 44 | } 45 | b, err := proto.Marshal(pushMsg) 46 | if err != nil { 47 | return 48 | } 49 | m := &sarama.ProducerMessage{ 50 | Key: sarama.StringEncoder(room), 51 | Topic: d.c.Kafka.Topic, 52 | Value: sarama.ByteEncoder(b), 53 | } 54 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil { 55 | log.Errorf("PushMsg.send(broadcast_room pushMsg:%v) error(%v)", pushMsg, err) 56 | } 57 | return 58 | } 59 | 60 | // BroadcastMsg push a message to databus. 61 | func (d *Dao) BroadcastMsg(c context.Context, op, speed int32, msg []byte) (err error) { 62 | pushMsg := &pb.PushMsg{ 63 | Type: pb.PushMsg_BROADCAST, 64 | Operation: op, 65 | Speed: speed, 66 | Msg: msg, 67 | } 68 | b, err := proto.Marshal(pushMsg) 69 | if err != nil { 70 | return 71 | } 72 | m := &sarama.ProducerMessage{ 73 | Key: sarama.StringEncoder(strconv.FormatInt(int64(op), 10)), 74 | Topic: d.c.Kafka.Topic, 75 | Value: sarama.ByteEncoder(b), 76 | } 77 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil { 78 | log.Errorf("PushMsg.send(broadcast pushMsg:%v) error(%v)", pushMsg, err) 79 | } 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /internal/logic/dao/kafka_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDaoPushMsg(t *testing.T) { 11 | var ( 12 | c = context.Background() 13 | op = int32(100) 14 | server = "test" 15 | msg = []byte("msg") 16 | keys = []string{"key"} 17 | ) 18 | err := d.PushMsg(c, op, server, keys, msg) 19 | assert.Nil(t, err) 20 | } 21 | 22 | func TestDaoBroadcastRoomMsg(t *testing.T) { 23 | var ( 24 | c = context.Background() 25 | op = int32(100) 26 | room = "test://1" 27 | msg = []byte("msg") 28 | ) 29 | err := d.BroadcastRoomMsg(c, op, room, msg) 30 | assert.Nil(t, err) 31 | } 32 | 33 | func TestDaoBroadcastMsg(t *testing.T) { 34 | var ( 35 | c = context.Background() 36 | op = int32(100) 37 | speed = int32(0) 38 | msg = []byte("") 39 | ) 40 | err := d.BroadcastMsg(c, op, speed, msg) 41 | assert.Nil(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /internal/logic/dao/redis_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Terry-Mao/goim/internal/logic/model" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDaopingRedis(t *testing.T) { 12 | err := d.pingRedis(context.Background()) 13 | assert.Nil(t, err) 14 | } 15 | 16 | func TestDaoAddMapping(t *testing.T) { 17 | var ( 18 | c = context.Background() 19 | mid = int64(1) 20 | key = "test_key" 21 | server = "test_server" 22 | ) 23 | err := d.AddMapping(c, 0, "test", server) 24 | assert.Nil(t, err) 25 | err = d.AddMapping(c, mid, key, server) 26 | assert.Nil(t, err) 27 | 28 | has, err := d.ExpireMapping(c, 0, "test") 29 | assert.Nil(t, err) 30 | assert.NotEqual(t, false, has) 31 | has, err = d.ExpireMapping(c, mid, key) 32 | assert.Nil(t, err) 33 | assert.NotEqual(t, false, has) 34 | 35 | res, err := d.ServersByKeys(c, []string{key}) 36 | assert.Nil(t, err) 37 | assert.Equal(t, server, res[0]) 38 | 39 | ress, mids, err := d.KeysByMids(c, []int64{mid}) 40 | assert.Nil(t, err) 41 | assert.Equal(t, server, ress[key]) 42 | assert.Equal(t, mid, mids[0]) 43 | 44 | has, err = d.DelMapping(c, 0, "test", server) 45 | assert.Nil(t, err) 46 | assert.NotEqual(t, false, has) 47 | has, err = d.DelMapping(c, mid, key, server) 48 | assert.Nil(t, err) 49 | assert.NotEqual(t, false, has) 50 | } 51 | 52 | func TestDaoAddServerOnline(t *testing.T) { 53 | var ( 54 | c = context.Background() 55 | server = "test_server" 56 | online = &model.Online{ 57 | RoomCount: map[string]int32{"room": 10}, 58 | } 59 | ) 60 | err := d.AddServerOnline(c, server, online) 61 | assert.Nil(t, err) 62 | 63 | r, err := d.ServerOnline(c, server) 64 | assert.Nil(t, err) 65 | assert.Equal(t, online.RoomCount["room"], r.RoomCount["room"]) 66 | 67 | err = d.DelServerOnline(c, server) 68 | assert.Nil(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /internal/logic/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | pb "github.com/Terry-Mao/goim/api/logic" 9 | "github.com/Terry-Mao/goim/internal/logic" 10 | "github.com/Terry-Mao/goim/internal/logic/conf" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/keepalive" 14 | 15 | // use gzip decoder 16 | _ "google.golang.org/grpc/encoding/gzip" 17 | ) 18 | 19 | // New logic grpc server 20 | func New(c *conf.RPCServer, l *logic.Logic) *grpc.Server { 21 | keepParams := grpc.KeepaliveParams(keepalive.ServerParameters{ 22 | MaxConnectionIdle: time.Duration(c.IdleTimeout), 23 | MaxConnectionAgeGrace: time.Duration(c.ForceCloseWait), 24 | Time: time.Duration(c.KeepAliveInterval), 25 | Timeout: time.Duration(c.KeepAliveTimeout), 26 | MaxConnectionAge: time.Duration(c.MaxLifeTime), 27 | }) 28 | srv := grpc.NewServer(keepParams) 29 | pb.RegisterLogicServer(srv, &server{l}) 30 | lis, err := net.Listen(c.Network, c.Addr) 31 | if err != nil { 32 | panic(err) 33 | } 34 | go func() { 35 | if err := srv.Serve(lis); err != nil { 36 | panic(err) 37 | } 38 | }() 39 | return srv 40 | } 41 | 42 | type server struct { 43 | srv *logic.Logic 44 | } 45 | 46 | var _ pb.LogicServer = &server{} 47 | 48 | // Connect connect a conn. 49 | func (s *server) Connect(ctx context.Context, req *pb.ConnectReq) (*pb.ConnectReply, error) { 50 | mid, key, room, accepts, hb, err := s.srv.Connect(ctx, req.Server, req.Cookie, req.Token) 51 | if err != nil { 52 | return &pb.ConnectReply{}, err 53 | } 54 | return &pb.ConnectReply{Mid: mid, Key: key, RoomID: room, Accepts: accepts, Heartbeat: hb}, nil 55 | } 56 | 57 | // Disconnect disconnect a conn. 58 | func (s *server) Disconnect(ctx context.Context, req *pb.DisconnectReq) (*pb.DisconnectReply, error) { 59 | has, err := s.srv.Disconnect(ctx, req.Mid, req.Key, req.Server) 60 | if err != nil { 61 | return &pb.DisconnectReply{}, err 62 | } 63 | return &pb.DisconnectReply{Has: has}, nil 64 | } 65 | 66 | // Heartbeat beartbeat a conn. 67 | func (s *server) Heartbeat(ctx context.Context, req *pb.HeartbeatReq) (*pb.HeartbeatReply, error) { 68 | if err := s.srv.Heartbeat(ctx, req.Mid, req.Key, req.Server); err != nil { 69 | return &pb.HeartbeatReply{}, err 70 | } 71 | return &pb.HeartbeatReply{}, nil 72 | } 73 | 74 | // RenewOnline renew server online. 75 | func (s *server) RenewOnline(ctx context.Context, req *pb.OnlineReq) (*pb.OnlineReply, error) { 76 | allRoomCount, err := s.srv.RenewOnline(ctx, req.Server, req.RoomCount) 77 | if err != nil { 78 | return &pb.OnlineReply{}, err 79 | } 80 | return &pb.OnlineReply{AllRoomCount: allRoomCount}, nil 81 | } 82 | 83 | // Receive receive a message. 84 | func (s *server) Receive(ctx context.Context, req *pb.ReceiveReq) (*pb.ReceiveReply, error) { 85 | if err := s.srv.Receive(ctx, req.Mid, req.Proto); err != nil { 86 | return &pb.ReceiveReply{}, err 87 | } 88 | return &pb.ReceiveReply{}, nil 89 | } 90 | 91 | // nodes return nodes. 92 | func (s *server) Nodes(ctx context.Context, req *pb.NodesReq) (*pb.NodesReply, error) { 93 | return s.srv.NodesWeighted(ctx, req.Platform, req.ClientIP), nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/logic/http/middleware.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http/httputil" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | log "github.com/golang/glog" 11 | ) 12 | 13 | func loggerHandler(c *gin.Context) { 14 | // Start timer 15 | start := time.Now() 16 | path := c.Request.URL.Path 17 | raw := c.Request.URL.RawQuery 18 | method := c.Request.Method 19 | 20 | // Process request 21 | c.Next() 22 | 23 | // Stop timer 24 | end := time.Now() 25 | latency := end.Sub(start) 26 | statusCode := c.Writer.Status() 27 | ecode := c.GetInt(contextErrCode) 28 | clientIP := c.ClientIP() 29 | if raw != "" { 30 | path = path + "?" + raw 31 | } 32 | log.Infof("METHOD:%s | PATH:%s | CODE:%d | IP:%s | TIME:%d | ECODE:%d", method, path, statusCode, clientIP, latency/time.Millisecond, ecode) 33 | } 34 | 35 | func recoverHandler(c *gin.Context) { 36 | defer func() { 37 | if err := recover(); err != nil { 38 | const size = 64 << 10 39 | buf := make([]byte, size) 40 | buf = buf[:runtime.Stack(buf, false)] 41 | httprequest, _ := httputil.DumpRequest(c.Request, false) 42 | pnc := fmt.Sprintf("[Recovery] %s panic recovered:\n%s\n%s\n%s", time.Now().Format("2006-01-02 15:04:05"), string(httprequest), err, buf) 43 | fmt.Print(pnc) 44 | log.Error(pnc) 45 | c.AbortWithStatus(500) 46 | } 47 | }() 48 | c.Next() 49 | } 50 | -------------------------------------------------------------------------------- /internal/logic/http/nodes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (s *Server) nodesWeighted(c *gin.Context) { 10 | var arg struct { 11 | Platform string `form:"platform"` 12 | } 13 | if err := c.BindQuery(&arg); err != nil { 14 | errors(c, RequestErr, err.Error()) 15 | return 16 | } 17 | res := s.logic.NodesWeighted(c, arg.Platform, c.ClientIP()) 18 | result(c, res, OK) 19 | } 20 | 21 | func (s *Server) nodesInstances(c *gin.Context) { 22 | res := s.logic.NodesInstances(context.TODO()) 23 | result(c, res, OK) 24 | } 25 | -------------------------------------------------------------------------------- /internal/logic/http/online.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (s *Server) onlineTop(c *gin.Context) { 10 | var arg struct { 11 | Type string `form:"type" binding:"required"` 12 | Limit int `form:"limit" binding:"required"` 13 | } 14 | if err := c.BindQuery(&arg); err != nil { 15 | errors(c, RequestErr, err.Error()) 16 | return 17 | } 18 | res, err := s.logic.OnlineTop(c, arg.Type, arg.Limit) 19 | if err != nil { 20 | result(c, nil, RequestErr) 21 | return 22 | } 23 | result(c, res, OK) 24 | } 25 | 26 | func (s *Server) onlineRoom(c *gin.Context) { 27 | var arg struct { 28 | Type string `form:"type" binding:"required"` 29 | Rooms []string `form:"rooms" binding:"required"` 30 | } 31 | if err := c.BindQuery(&arg); err != nil { 32 | errors(c, RequestErr, err.Error()) 33 | return 34 | } 35 | res, err := s.logic.OnlineRoom(c, arg.Type, arg.Rooms) 36 | if err != nil { 37 | result(c, nil, RequestErr) 38 | return 39 | } 40 | result(c, res, OK) 41 | } 42 | 43 | func (s *Server) onlineTotal(c *gin.Context) { 44 | ipCount, connCount := s.logic.OnlineTotal(context.TODO()) 45 | res := map[string]interface{}{ 46 | "ip_count": ipCount, 47 | "conn_count": connCount, 48 | } 49 | result(c, res, OK) 50 | } 51 | -------------------------------------------------------------------------------- /internal/logic/http/push.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func (s *Server) pushKeys(c *gin.Context) { 11 | var arg struct { 12 | Op int32 `form:"operation"` 13 | Keys []string `form:"keys"` 14 | } 15 | if err := c.BindQuery(&arg); err != nil { 16 | errors(c, RequestErr, err.Error()) 17 | return 18 | } 19 | // read message 20 | msg, err := ioutil.ReadAll(c.Request.Body) 21 | if err != nil { 22 | errors(c, RequestErr, err.Error()) 23 | return 24 | } 25 | if err = s.logic.PushKeys(context.TODO(), arg.Op, arg.Keys, msg); err != nil { 26 | result(c, nil, RequestErr) 27 | return 28 | } 29 | result(c, nil, OK) 30 | } 31 | 32 | func (s *Server) pushMids(c *gin.Context) { 33 | var arg struct { 34 | Op int32 `form:"operation"` 35 | Mids []int64 `form:"mids"` 36 | } 37 | if err := c.BindQuery(&arg); err != nil { 38 | errors(c, RequestErr, err.Error()) 39 | return 40 | } 41 | // read message 42 | msg, err := ioutil.ReadAll(c.Request.Body) 43 | if err != nil { 44 | errors(c, RequestErr, err.Error()) 45 | return 46 | } 47 | if err = s.logic.PushMids(context.TODO(), arg.Op, arg.Mids, msg); err != nil { 48 | errors(c, ServerErr, err.Error()) 49 | return 50 | } 51 | result(c, nil, OK) 52 | } 53 | 54 | func (s *Server) pushRoom(c *gin.Context) { 55 | var arg struct { 56 | Op int32 `form:"operation" binding:"required"` 57 | Type string `form:"type" binding:"required"` 58 | Room string `form:"room" binding:"required"` 59 | } 60 | if err := c.BindQuery(&arg); err != nil { 61 | errors(c, RequestErr, err.Error()) 62 | return 63 | } 64 | // read message 65 | msg, err := ioutil.ReadAll(c.Request.Body) 66 | if err != nil { 67 | errors(c, RequestErr, err.Error()) 68 | return 69 | } 70 | if err = s.logic.PushRoom(c, arg.Op, arg.Type, arg.Room, msg); err != nil { 71 | errors(c, ServerErr, err.Error()) 72 | return 73 | } 74 | result(c, nil, OK) 75 | } 76 | 77 | func (s *Server) pushAll(c *gin.Context) { 78 | var arg struct { 79 | Op int32 `form:"operation" binding:"required"` 80 | Speed int32 `form:"speed"` 81 | } 82 | if err := c.BindQuery(&arg); err != nil { 83 | errors(c, RequestErr, err.Error()) 84 | return 85 | } 86 | msg, err := ioutil.ReadAll(c.Request.Body) 87 | if err != nil { 88 | errors(c, RequestErr, err.Error()) 89 | return 90 | } 91 | if err = s.logic.PushAll(c, arg.Op, arg.Speed, msg); err != nil { 92 | errors(c, ServerErr, err.Error()) 93 | return 94 | } 95 | result(c, nil, OK) 96 | } 97 | -------------------------------------------------------------------------------- /internal/logic/http/result.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | const ( 8 | // OK ok 9 | OK = 0 10 | // RequestErr request error 11 | RequestErr = -400 12 | // ServerErr server error 13 | ServerErr = -500 14 | 15 | contextErrCode = "context/err/code" 16 | ) 17 | 18 | type resp struct { 19 | Code int `json:"code"` 20 | Message string `json:"message"` 21 | Data interface{} `json:"data,omitempty"` 22 | } 23 | 24 | func errors(c *gin.Context, code int, msg string) { 25 | c.Set(contextErrCode, code) 26 | c.JSON(200, resp{ 27 | Code: code, 28 | Message: msg, 29 | }) 30 | } 31 | 32 | func result(c *gin.Context, data interface{}, code int) { 33 | c.Set(contextErrCode, code) 34 | c.JSON(200, resp{ 35 | Code: code, 36 | Data: data, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/logic/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/Terry-Mao/goim/internal/logic" 5 | "github.com/Terry-Mao/goim/internal/logic/conf" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Server is http server. 11 | type Server struct { 12 | engine *gin.Engine 13 | logic *logic.Logic 14 | } 15 | 16 | // New new a http server. 17 | func New(c *conf.HTTPServer, l *logic.Logic) *Server { 18 | engine := gin.New() 19 | engine.Use(loggerHandler, recoverHandler) 20 | go func() { 21 | if err := engine.Run(c.Addr); err != nil { 22 | panic(err) 23 | } 24 | }() 25 | s := &Server{ 26 | engine: engine, 27 | logic: l, 28 | } 29 | s.initRouter() 30 | return s 31 | } 32 | 33 | func (s *Server) initRouter() { 34 | group := s.engine.Group("/goim") 35 | group.POST("/push/keys", s.pushKeys) 36 | group.POST("/push/mids", s.pushMids) 37 | group.POST("/push/room", s.pushRoom) 38 | group.POST("/push/all", s.pushAll) 39 | group.GET("/online/top", s.onlineTop) 40 | group.GET("/online/room", s.onlineRoom) 41 | group.GET("/online/total", s.onlineTotal) 42 | group.GET("/nodes/weighted", s.nodesWeighted) 43 | group.GET("/nodes/instances", s.nodesInstances) 44 | } 45 | 46 | // Close close the server. 47 | func (s *Server) Close() { 48 | 49 | } 50 | -------------------------------------------------------------------------------- /internal/logic/logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/Terry-Mao/goim/internal/logic/conf" 9 | "github.com/Terry-Mao/goim/internal/logic/dao" 10 | "github.com/Terry-Mao/goim/internal/logic/model" 11 | "github.com/bilibili/discovery/naming" 12 | log "github.com/golang/glog" 13 | ) 14 | 15 | const ( 16 | _onlineTick = time.Second * 10 17 | _onlineDeadline = time.Minute * 5 18 | ) 19 | 20 | // Logic struct 21 | type Logic struct { 22 | c *conf.Config 23 | dis *naming.Discovery 24 | dao *dao.Dao 25 | // online 26 | totalIPs int64 27 | totalConns int64 28 | roomCount map[string]int32 29 | // load balancer 30 | nodes []*naming.Instance 31 | loadBalancer *LoadBalancer 32 | regions map[string]string // province -> region 33 | } 34 | 35 | // New init 36 | func New(c *conf.Config) (l *Logic) { 37 | l = &Logic{ 38 | c: c, 39 | dao: dao.New(c), 40 | dis: naming.New(c.Discovery), 41 | loadBalancer: NewLoadBalancer(), 42 | regions: make(map[string]string), 43 | } 44 | l.initRegions() 45 | l.initNodes() 46 | _ = l.loadOnline() 47 | go l.onlineproc() 48 | return l 49 | } 50 | 51 | // Ping ping resources is ok. 52 | func (l *Logic) Ping(c context.Context) (err error) { 53 | return l.dao.Ping(c) 54 | } 55 | 56 | // Close close resources. 57 | func (l *Logic) Close() { 58 | l.dao.Close() 59 | } 60 | 61 | func (l *Logic) initRegions() { 62 | for region, ps := range l.c.Regions { 63 | for _, province := range ps { 64 | l.regions[province] = region 65 | } 66 | } 67 | } 68 | 69 | func (l *Logic) initNodes() { 70 | res := l.dis.Build("goim.comet") 71 | event := res.Watch() 72 | select { 73 | case _, ok := <-event: 74 | if ok { 75 | l.newNodes(res) 76 | } else { 77 | panic("discovery watch failed") 78 | } 79 | case <-time.After(10 * time.Second): 80 | log.Error("discovery start timeout") 81 | } 82 | go func() { 83 | for { 84 | if _, ok := <-event; !ok { 85 | return 86 | } 87 | l.newNodes(res) 88 | } 89 | }() 90 | } 91 | 92 | func (l *Logic) newNodes(res naming.Resolver) { 93 | if zoneIns, ok := res.Fetch(); ok { 94 | var ( 95 | totalConns int64 96 | totalIPs int64 97 | allIns []*naming.Instance 98 | ) 99 | for _, zins := range zoneIns.Instances { 100 | for _, ins := range zins { 101 | if ins.Metadata == nil { 102 | log.Errorf("node instance metadata is empty(%+v)", ins) 103 | continue 104 | } 105 | offline, err := strconv.ParseBool(ins.Metadata[model.MetaOffline]) 106 | if err != nil || offline { 107 | log.Warningf("strconv.ParseBool(offline:%t) error(%v)", offline, err) 108 | continue 109 | } 110 | conns, err := strconv.ParseInt(ins.Metadata[model.MetaConnCount], 10, 32) 111 | if err != nil { 112 | log.Errorf("strconv.ParseInt(conns:%d) error(%v)", conns, err) 113 | continue 114 | } 115 | ips, err := strconv.ParseInt(ins.Metadata[model.MetaIPCount], 10, 32) 116 | if err != nil { 117 | log.Errorf("strconv.ParseInt(ips:%d) error(%v)", ips, err) 118 | continue 119 | } 120 | totalConns += conns 121 | totalIPs += ips 122 | allIns = append(allIns, ins) 123 | } 124 | } 125 | l.totalConns = totalConns 126 | l.totalIPs = totalIPs 127 | l.nodes = allIns 128 | l.loadBalancer.Update(allIns) 129 | } 130 | } 131 | 132 | func (l *Logic) onlineproc() { 133 | for { 134 | time.Sleep(_onlineTick) 135 | if err := l.loadOnline(); err != nil { 136 | log.Errorf("onlineproc error(%v)", err) 137 | } 138 | } 139 | } 140 | 141 | func (l *Logic) loadOnline() (err error) { 142 | var ( 143 | roomCount = make(map[string]int32) 144 | ) 145 | for _, server := range l.nodes { 146 | var online *model.Online 147 | online, err = l.dao.ServerOnline(context.Background(), server.Hostname) 148 | if err != nil { 149 | return 150 | } 151 | if time.Since(time.Unix(online.Updated, 0)) > _onlineDeadline { 152 | _ = l.dao.DelServerOnline(context.Background(), server.Hostname) 153 | continue 154 | } 155 | for roomID, count := range online.RoomCount { 156 | roomCount[roomID] += count 157 | } 158 | } 159 | l.roomCount = roomCount 160 | return 161 | } 162 | -------------------------------------------------------------------------------- /internal/logic/logic_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "os" 7 | "testing" 8 | 9 | "github.com/Terry-Mao/goim/internal/logic/conf" 10 | ) 11 | 12 | var ( 13 | lg *Logic 14 | ) 15 | 16 | func TestMain(m *testing.M) { 17 | if err := flag.Set("conf", "../../cmd/logic/logic-example.toml"); err != nil { 18 | panic(err) 19 | } 20 | flag.Parse() 21 | if err := conf.Init(); err != nil { 22 | panic(err) 23 | } 24 | lg = New(conf.Conf) 25 | if err := lg.Ping(context.TODO()); err != nil { 26 | panic(err) 27 | } 28 | os.Exit(m.Run()) 29 | } 30 | -------------------------------------------------------------------------------- /internal/logic/model/metadata.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | // MetaWeight meta weight 5 | MetaWeight = "weight" 6 | // MetaOffline meta offline 7 | MetaOffline = "offline" 8 | // MetaAddrs meta public ip addrs 9 | MetaAddrs = "addrs" 10 | // MetaIPCount meta ip count 11 | MetaIPCount = "ip_count" 12 | // MetaConnCount meta conn count 13 | MetaConnCount = "conn_count" 14 | 15 | // PlatformWeb platform web 16 | PlatformWeb = "web" 17 | ) 18 | -------------------------------------------------------------------------------- /internal/logic/model/online.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Online ip and room online. 4 | type Online struct { 5 | Server string `json:"server"` 6 | RoomCount map[string]int32 `json:"room_count"` 7 | Updated int64 `json:"updated"` 8 | } 9 | 10 | // Top top sorted. 11 | type Top struct { 12 | RoomID string `json:"room_id"` 13 | Count int32 `json:"count"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/logic/model/room.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // EncodeRoomKey encode a room key. 9 | func EncodeRoomKey(typ string, room string) string { 10 | return fmt.Sprintf("%s://%s", typ, room) 11 | } 12 | 13 | // DecodeRoomKey decode room key. 14 | func DecodeRoomKey(key string) (string, string, error) { 15 | u, err := url.Parse(key) 16 | if err != nil { 17 | return "", "", err 18 | } 19 | return u.Scheme, u.Host, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/logic/nodes.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | pb "github.com/Terry-Mao/goim/api/logic" 8 | "github.com/Terry-Mao/goim/internal/logic/model" 9 | "github.com/bilibili/discovery/naming" 10 | log "github.com/golang/glog" 11 | ) 12 | 13 | // NodesInstances get servers info. 14 | func (l *Logic) NodesInstances(c context.Context) (res []*naming.Instance) { 15 | return l.nodes 16 | } 17 | 18 | // NodesWeighted get node list. 19 | func (l *Logic) NodesWeighted(c context.Context, platform, clientIP string) *pb.NodesReply { 20 | reply := &pb.NodesReply{ 21 | Domain: l.c.Node.DefaultDomain, 22 | TcpPort: int32(l.c.Node.TCPPort), 23 | WsPort: int32(l.c.Node.WSPort), 24 | WssPort: int32(l.c.Node.WSSPort), 25 | Heartbeat: int32(time.Duration(l.c.Node.Heartbeat) / time.Second), 26 | HeartbeatMax: int32(l.c.Node.HeartbeatMax), 27 | Backoff: &pb.Backoff{ 28 | MaxDelay: l.c.Backoff.MaxDelay, 29 | BaseDelay: l.c.Backoff.BaseDelay, 30 | Factor: l.c.Backoff.Factor, 31 | Jitter: l.c.Backoff.Jitter, 32 | }, 33 | } 34 | domains, addrs := l.nodeAddrs(c, clientIP) 35 | if platform == model.PlatformWeb { 36 | reply.Nodes = domains 37 | } else { 38 | reply.Nodes = addrs 39 | } 40 | if len(reply.Nodes) == 0 { 41 | reply.Nodes = []string{l.c.Node.DefaultDomain} 42 | } 43 | return reply 44 | } 45 | 46 | func (l *Logic) nodeAddrs(c context.Context, clientIP string) (domains, addrs []string) { 47 | var ( 48 | region string 49 | ) 50 | province, err := l.location(c, clientIP) 51 | if err == nil { 52 | region = l.regions[province] 53 | } 54 | log.Infof("nodeAddrs clientIP:%s region:%s province:%s domains:%v addrs:%v", clientIP, region, province, domains, addrs) 55 | return l.loadBalancer.NodeAddrs(region, l.c.Node.HostDomain, l.c.Node.RegionWeight) 56 | } 57 | 58 | // location find a geolocation of an IP address including province, region and country. 59 | func (l *Logic) location(c context.Context, clientIP string) (province string, err error) { 60 | // province: config mapping 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /internal/logic/nodes_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/bilibili/discovery/naming" 8 | "github.com/Terry-Mao/goim/internal/logic/model" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNodes(t *testing.T) { 14 | var ( 15 | c = context.TODO() 16 | clientIP = "127.0.0.1" 17 | ) 18 | lg.nodes = make([]*naming.Instance, 0) 19 | ins := lg.NodesInstances(c) 20 | assert.NotNil(t, ins) 21 | nodes := lg.NodesWeighted(c, model.PlatformWeb, clientIP) 22 | assert.NotNil(t, nodes) 23 | nodes = lg.NodesWeighted(c, "android", clientIP) 24 | assert.NotNil(t, nodes) 25 | } 26 | -------------------------------------------------------------------------------- /internal/logic/online.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/Terry-Mao/goim/internal/logic/model" 9 | ) 10 | 11 | var ( 12 | _emptyTops = make([]*model.Top, 0) 13 | ) 14 | 15 | // OnlineTop get the top online. 16 | func (l *Logic) OnlineTop(c context.Context, typ string, n int) (tops []*model.Top, err error) { 17 | for key, cnt := range l.roomCount { 18 | if strings.HasPrefix(key, typ) { 19 | _, roomID, err := model.DecodeRoomKey(key) 20 | if err != nil { 21 | continue 22 | } 23 | top := &model.Top{ 24 | RoomID: roomID, 25 | Count: cnt, 26 | } 27 | tops = append(tops, top) 28 | } 29 | } 30 | sort.Slice(tops, func(i, j int) bool { 31 | return tops[i].Count > tops[j].Count 32 | }) 33 | if len(tops) > n { 34 | tops = tops[:n] 35 | } 36 | if len(tops) == 0 { 37 | tops = _emptyTops 38 | } 39 | return 40 | } 41 | 42 | // OnlineRoom get rooms online. 43 | func (l *Logic) OnlineRoom(c context.Context, typ string, rooms []string) (res map[string]int32, err error) { 44 | res = make(map[string]int32, len(rooms)) 45 | for _, room := range rooms { 46 | res[room] = l.roomCount[model.EncodeRoomKey(typ, room)] 47 | } 48 | return 49 | } 50 | 51 | // OnlineTotal get all online. 52 | func (l *Logic) OnlineTotal(c context.Context) (int64, int64) { 53 | return l.totalIPs, l.totalConns 54 | } 55 | -------------------------------------------------------------------------------- /internal/logic/online_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestOnline(t *testing.T) { 11 | var ( 12 | c = context.TODO() 13 | typ = "test" 14 | n = 2 15 | rooms = []string{"room_01", "room_02", "room_03"} 16 | ) 17 | lg.totalIPs = 100 18 | lg.totalConns = 200 19 | lg.roomCount = map[string]int32{ 20 | "test://room_01": 100, 21 | "test://room_02": 200, 22 | "test://room_03": 300, 23 | } 24 | tops, err := lg.OnlineTop(c, typ, n) 25 | assert.Nil(t, err) 26 | assert.Equal(t, len(tops), 2) 27 | onlines, err := lg.OnlineRoom(c, typ, rooms) 28 | assert.Nil(t, err) 29 | assert.Equal(t, onlines["room_01"], int32(100)) 30 | assert.Equal(t, onlines["room_02"], int32(200)) 31 | assert.Equal(t, onlines["room_03"], int32(300)) 32 | ips, conns := lg.OnlineTotal(c) 33 | assert.Equal(t, ips, int64(100)) 34 | assert.Equal(t, conns, int64(200)) 35 | } 36 | -------------------------------------------------------------------------------- /internal/logic/push.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Terry-Mao/goim/internal/logic/model" 7 | 8 | log "github.com/golang/glog" 9 | ) 10 | 11 | // PushKeys push a message by keys. 12 | func (l *Logic) PushKeys(c context.Context, op int32, keys []string, msg []byte) (err error) { 13 | servers, err := l.dao.ServersByKeys(c, keys) 14 | if err != nil { 15 | return 16 | } 17 | pushKeys := make(map[string][]string) 18 | for i, key := range keys { 19 | server := servers[i] 20 | if server != "" && key != "" { 21 | pushKeys[server] = append(pushKeys[server], key) 22 | } 23 | } 24 | for server := range pushKeys { 25 | if err = l.dao.PushMsg(c, op, server, pushKeys[server], msg); err != nil { 26 | return 27 | } 28 | } 29 | return 30 | } 31 | 32 | // PushMids push a message by mid. 33 | func (l *Logic) PushMids(c context.Context, op int32, mids []int64, msg []byte) (err error) { 34 | keyServers, _, err := l.dao.KeysByMids(c, mids) 35 | if err != nil { 36 | return 37 | } 38 | keys := make(map[string][]string) 39 | for key, server := range keyServers { 40 | if key == "" || server == "" { 41 | log.Warningf("push key:%s server:%s is empty", key, server) 42 | continue 43 | } 44 | keys[server] = append(keys[server], key) 45 | } 46 | for server, keys := range keys { 47 | if err = l.dao.PushMsg(c, op, server, keys, msg); err != nil { 48 | return 49 | } 50 | } 51 | return 52 | } 53 | 54 | // PushRoom push a message by room. 55 | func (l *Logic) PushRoom(c context.Context, op int32, typ, room string, msg []byte) (err error) { 56 | return l.dao.BroadcastRoomMsg(c, op, model.EncodeRoomKey(typ, room), msg) 57 | } 58 | 59 | // PushAll push a message to all. 60 | func (l *Logic) PushAll(c context.Context, op, speed int32, msg []byte) (err error) { 61 | return l.dao.BroadcastMsg(c, op, speed, msg) 62 | } 63 | -------------------------------------------------------------------------------- /internal/logic/push_test.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPushKeys(t *testing.T) { 11 | var ( 12 | c = context.TODO() 13 | op = int32(100) 14 | keys = []string{"test_key"} 15 | msg = []byte("hello") 16 | ) 17 | err := lg.PushKeys(c, op, keys, msg) 18 | assert.Nil(t, err) 19 | } 20 | 21 | func TestPushMids(t *testing.T) { 22 | var ( 23 | c = context.TODO() 24 | op = int32(100) 25 | mids = []int64{1, 2, 3} 26 | msg = []byte("hello") 27 | ) 28 | err := lg.PushMids(c, op, mids, msg) 29 | assert.Nil(t, err) 30 | } 31 | 32 | func TestPushRoom(t *testing.T) { 33 | var ( 34 | c = context.TODO() 35 | op = int32(100) 36 | typ = "test" 37 | room = "test_room" 38 | msg = []byte("hello") 39 | ) 40 | err := lg.PushRoom(c, op, typ, room, msg) 41 | assert.Nil(t, err) 42 | } 43 | 44 | func TestPushAll(t *testing.T) { 45 | var ( 46 | c = context.TODO() 47 | op = int32(100) 48 | speed = int32(100) 49 | msg = []byte("hello") 50 | ) 51 | err := lg.PushAll(c, op, speed, msg) 52 | assert.Nil(t, err) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/bytes/buffer.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Buffer buffer. 8 | type Buffer struct { 9 | buf []byte 10 | next *Buffer // next free buffer 11 | } 12 | 13 | // Bytes bytes. 14 | func (b *Buffer) Bytes() []byte { 15 | return b.buf 16 | } 17 | 18 | // Pool is a buffer pool. 19 | type Pool struct { 20 | lock sync.Mutex 21 | free *Buffer 22 | max int 23 | num int 24 | size int 25 | } 26 | 27 | // NewPool new a memory buffer pool struct. 28 | func NewPool(num, size int) (p *Pool) { 29 | p = new(Pool) 30 | p.init(num, size) 31 | return 32 | } 33 | 34 | // Init init the memory buffer. 35 | func (p *Pool) Init(num, size int) { 36 | p.init(num, size) 37 | } 38 | 39 | // init init the memory buffer. 40 | func (p *Pool) init(num, size int) { 41 | p.num = num 42 | p.size = size 43 | p.max = num * size 44 | p.grow() 45 | } 46 | 47 | // grow grow the memory buffer size, and update free pointer. 48 | func (p *Pool) grow() { 49 | var ( 50 | i int 51 | b *Buffer 52 | bs []Buffer 53 | buf []byte 54 | ) 55 | buf = make([]byte, p.max) 56 | bs = make([]Buffer, p.num) 57 | p.free = &bs[0] 58 | b = p.free 59 | for i = 1; i < p.num; i++ { 60 | b.buf = buf[(i-1)*p.size : i*p.size] 61 | b.next = &bs[i] 62 | b = b.next 63 | } 64 | b.buf = buf[(i-1)*p.size : i*p.size] 65 | b.next = nil 66 | } 67 | 68 | // Get get a free memory buffer. 69 | func (p *Pool) Get() (b *Buffer) { 70 | p.lock.Lock() 71 | if b = p.free; b == nil { 72 | p.grow() 73 | b = p.free 74 | } 75 | p.free = b.next 76 | p.lock.Unlock() 77 | return 78 | } 79 | 80 | // Put put back a memory buffer to free. 81 | func (p *Pool) Put(b *Buffer) { 82 | p.lock.Lock() 83 | b.next = p.free 84 | p.free = b 85 | p.lock.Unlock() 86 | } 87 | -------------------------------------------------------------------------------- /pkg/bytes/buffer_test.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBuffer(t *testing.T) { 8 | p := NewPool(2, 10) 9 | b := p.Get() 10 | if b.Bytes() == nil || len(b.Bytes()) == 0 { 11 | t.FailNow() 12 | } 13 | b = p.Get() 14 | if b.Bytes() == nil || len(b.Bytes()) == 0 { 15 | t.FailNow() 16 | } 17 | b = p.Get() 18 | if b.Bytes() == nil || len(b.Bytes()) == 0 { 19 | t.FailNow() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/bytes/writer.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | // Writer writer. 4 | type Writer struct { 5 | n int 6 | buf []byte 7 | } 8 | 9 | // NewWriterSize new a writer with size. 10 | func NewWriterSize(n int) *Writer { 11 | return &Writer{buf: make([]byte, n)} 12 | } 13 | 14 | // Len buff len. 15 | func (w *Writer) Len() int { 16 | return w.n 17 | } 18 | 19 | // Size buff cap. 20 | func (w *Writer) Size() int { 21 | return len(w.buf) 22 | } 23 | 24 | // Reset reset the buff. 25 | func (w *Writer) Reset() { 26 | w.n = 0 27 | } 28 | 29 | // Buffer return buff. 30 | func (w *Writer) Buffer() []byte { 31 | return w.buf[:w.n] 32 | } 33 | 34 | // Peek peek a buf. 35 | func (w *Writer) Peek(n int) []byte { 36 | var buf []byte 37 | w.grow(n) 38 | buf = w.buf[w.n : w.n+n] 39 | w.n += n 40 | return buf 41 | } 42 | 43 | // Write write a buff. 44 | func (w *Writer) Write(p []byte) { 45 | w.grow(len(p)) 46 | w.n += copy(w.buf[w.n:], p) 47 | } 48 | 49 | func (w *Writer) grow(n int) { 50 | var buf []byte 51 | if w.n+n < len(w.buf) { 52 | return 53 | } 54 | buf = make([]byte, 2*len(w.buf)+n) 55 | copy(buf, w.buf[:w.n]) 56 | w.buf = buf 57 | } 58 | -------------------------------------------------------------------------------- /pkg/bytes/writer_test.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWriter(t *testing.T) { 9 | w := NewWriterSize(64) 10 | if w.Len() != 0 && w.Size() != 64 { 11 | t.FailNow() 12 | } 13 | b := []byte("hello") 14 | w.Write(b) 15 | if !reflect.DeepEqual(b, w.Buffer()) { 16 | t.FailNow() 17 | } 18 | w.Peek(len(b)) 19 | w.Reset() 20 | for i := 0; i < 1024; i++ { 21 | w.Write(b) 22 | } 23 | w.Reset() 24 | if w.Len() != 0 { 25 | t.FailNow() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/encoding/binary/endian.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | // BigEndian big endian. 4 | var BigEndian bigEndian 5 | 6 | type bigEndian struct{} 7 | 8 | func (bigEndian) Int8(b []byte) int8 { return int8(b[0]) } 9 | 10 | func (bigEndian) PutInt8(b []byte, v int8) { 11 | b[0] = byte(v) 12 | } 13 | 14 | func (bigEndian) Int16(b []byte) int16 { return int16(b[1]) | int16(b[0])<<8 } 15 | 16 | func (bigEndian) PutInt16(b []byte, v int16) { 17 | _ = b[1] 18 | b[0] = byte(v >> 8) 19 | b[1] = byte(v) 20 | } 21 | 22 | func (bigEndian) Int32(b []byte) int32 { 23 | return int32(b[3]) | int32(b[2])<<8 | int32(b[1])<<16 | int32(b[0])<<24 24 | } 25 | 26 | func (bigEndian) PutInt32(b []byte, v int32) { 27 | _ = b[3] 28 | b[0] = byte(v >> 24) 29 | b[1] = byte(v >> 16) 30 | b[2] = byte(v >> 8) 31 | b[3] = byte(v) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/encoding/binary/endian_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import "testing" 4 | 5 | func TestInt8(t *testing.T) { 6 | b := make([]byte, 1) 7 | BigEndian.PutInt8(b, 100) 8 | i := BigEndian.Int8(b) 9 | if i != 100 { 10 | t.FailNow() 11 | } 12 | } 13 | 14 | func TestInt16(t *testing.T) { 15 | b := make([]byte, 2) 16 | BigEndian.PutInt16(b, 100) 17 | i := BigEndian.Int16(b) 18 | if i != 100 { 19 | t.FailNow() 20 | } 21 | } 22 | 23 | func TestInt32(t *testing.T) { 24 | b := make([]byte, 4) 25 | BigEndian.PutInt32(b, 100) 26 | i := BigEndian.Int32(b) 27 | if i != 100 { 28 | t.FailNow() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/ip/ip.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | // InternalIP return internal ip. 9 | func InternalIP() string { 10 | inters, err := net.Interfaces() 11 | if err != nil { 12 | return "" 13 | } 14 | for _, inter := range inters { 15 | if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") { 16 | addrs, err := inter.Addrs() 17 | if err != nil { 18 | continue 19 | } 20 | for _, addr := range addrs { 21 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 22 | if ipnet.IP.To4() != nil { 23 | return ipnet.IP.String() 24 | } 25 | } 26 | } 27 | } 28 | } 29 | return "" 30 | } 31 | -------------------------------------------------------------------------------- /pkg/ip/ip_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import "testing" 4 | 5 | func TestIP(t *testing.T) { 6 | ip := InternalIP() 7 | if ip == "" { 8 | t.FailNow() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pkg/strings/ints.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | bfPool = sync.Pool{ 12 | New: func() interface{} { 13 | return bytes.NewBuffer([]byte{}) 14 | }, 15 | } 16 | ) 17 | 18 | // JoinInt32s format int32 slice like:n1,n2,n3. 19 | func JoinInt32s(is []int32, p string) string { 20 | if len(is) == 0 { 21 | return "" 22 | } 23 | if len(is) == 1 { 24 | return strconv.FormatInt(int64(is[0]), 10) 25 | } 26 | buf := bfPool.Get().(*bytes.Buffer) 27 | for _, i := range is { 28 | buf.WriteString(strconv.FormatInt(int64(i), 10)) 29 | buf.WriteString(p) 30 | } 31 | if buf.Len() > 0 { 32 | buf.Truncate(buf.Len() - 1) 33 | } 34 | s := buf.String() 35 | buf.Reset() 36 | bfPool.Put(buf) 37 | return s 38 | } 39 | 40 | // SplitInt32s split string into int32 slice. 41 | func SplitInt32s(s, p string) ([]int32, error) { 42 | if s == "" { 43 | return nil, nil 44 | } 45 | sArr := strings.Split(s, p) 46 | res := make([]int32, 0, len(sArr)) 47 | for _, sc := range sArr { 48 | i, err := strconv.ParseInt(sc, 10, 32) 49 | if err != nil { 50 | return nil, err 51 | } 52 | res = append(res, int32(i)) 53 | } 54 | return res, nil 55 | } 56 | 57 | // JoinInt64s format int64 slice like:n1,n2,n3. 58 | func JoinInt64s(is []int64, p string) string { 59 | if len(is) == 0 { 60 | return "" 61 | } 62 | if len(is) == 1 { 63 | return strconv.FormatInt(is[0], 10) 64 | } 65 | buf := bfPool.Get().(*bytes.Buffer) 66 | for _, i := range is { 67 | buf.WriteString(strconv.FormatInt(i, 10)) 68 | buf.WriteString(p) 69 | } 70 | if buf.Len() > 0 { 71 | buf.Truncate(buf.Len() - 1) 72 | } 73 | s := buf.String() 74 | buf.Reset() 75 | bfPool.Put(buf) 76 | return s 77 | } 78 | 79 | // SplitInt64s split string into int64 slice. 80 | func SplitInt64s(s, p string) ([]int64, error) { 81 | if s == "" { 82 | return nil, nil 83 | } 84 | sArr := strings.Split(s, p) 85 | res := make([]int64, 0, len(sArr)) 86 | for _, sc := range sArr { 87 | i, err := strconv.ParseInt(sc, 10, 64) 88 | if err != nil { 89 | return nil, err 90 | } 91 | res = append(res, i) 92 | } 93 | return res, nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/strings/ints_test.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestInt32(t *testing.T) { 9 | i := []int32{1, 2, 3} 10 | s := JoinInt32s(i, ",") 11 | ii, _ := SplitInt32s(s, ",") 12 | if !reflect.DeepEqual(i, ii) { 13 | t.FailNow() 14 | } 15 | } 16 | 17 | func TestInt64(t *testing.T) { 18 | i := []int64{1, 2, 3} 19 | s := JoinInt64s(i, ",") 20 | ii, _ := SplitInt64s(s, ",") 21 | if !reflect.DeepEqual(i, ii) { 22 | t.FailNow() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/time/debug.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | const ( 4 | // Debug debug switch 5 | Debug = false 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/time/duration.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | xtime "time" 5 | ) 6 | 7 | // Duration be used toml unmarshal string time, like 1s, 500ms. 8 | type Duration xtime.Duration 9 | 10 | // UnmarshalText unmarshal text to duration. 11 | func (d *Duration) UnmarshalText(text []byte) error { 12 | tmp, err := xtime.ParseDuration(string(text)) 13 | if err == nil { 14 | *d = Duration(tmp) 15 | } 16 | return err 17 | } 18 | -------------------------------------------------------------------------------- /pkg/time/duration_test.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDurationText(t *testing.T) { 9 | var ( 10 | input = []byte("10s") 11 | output = time.Second * 10 12 | d Duration 13 | ) 14 | if err := d.UnmarshalText(input); err != nil { 15 | t.FailNow() 16 | } 17 | if int64(output) != int64(d) { 18 | t.FailNow() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/time/timer.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "sync" 5 | itime "time" 6 | 7 | log "github.com/golang/glog" 8 | ) 9 | 10 | const ( 11 | timerFormat = "2006-01-02 15:04:05" 12 | infiniteDuration = itime.Duration(1<<63 - 1) 13 | ) 14 | 15 | // TimerData timer data. 16 | type TimerData struct { 17 | Key string 18 | expire itime.Time 19 | fn func() 20 | index int 21 | next *TimerData 22 | } 23 | 24 | // Delay delay duration. 25 | func (td *TimerData) Delay() itime.Duration { 26 | return itime.Until(td.expire) 27 | } 28 | 29 | // ExpireString expire string. 30 | func (td *TimerData) ExpireString() string { 31 | return td.expire.Format(timerFormat) 32 | } 33 | 34 | // Timer timer. 35 | type Timer struct { 36 | lock sync.Mutex 37 | free *TimerData 38 | timers []*TimerData 39 | signal *itime.Timer 40 | num int 41 | } 42 | 43 | // NewTimer new a timer. 44 | // A heap must be initialized before any of the heap operations 45 | // can be used. Init is idempotent with respect to the heap invariants 46 | // and may be called whenever the heap invariants may have been invalidated. 47 | // Its complexity is O(n) where n = h.Len(). 48 | // 49 | func NewTimer(num int) (t *Timer) { 50 | t = new(Timer) 51 | t.init(num) 52 | return t 53 | } 54 | 55 | // Init init the timer. 56 | func (t *Timer) Init(num int) { 57 | t.init(num) 58 | } 59 | 60 | func (t *Timer) init(num int) { 61 | t.signal = itime.NewTimer(infiniteDuration) 62 | t.timers = make([]*TimerData, 0, num) 63 | t.num = num 64 | t.grow() 65 | go t.start() 66 | } 67 | 68 | func (t *Timer) grow() { 69 | var ( 70 | i int 71 | td *TimerData 72 | tds = make([]TimerData, t.num) 73 | ) 74 | t.free = &(tds[0]) 75 | td = t.free 76 | for i = 1; i < t.num; i++ { 77 | td.next = &(tds[i]) 78 | td = td.next 79 | } 80 | td.next = nil 81 | } 82 | 83 | // get get a free timer data. 84 | func (t *Timer) get() (td *TimerData) { 85 | if td = t.free; td == nil { 86 | t.grow() 87 | td = t.free 88 | } 89 | t.free = td.next 90 | return 91 | } 92 | 93 | // put put back a timer data. 94 | func (t *Timer) put(td *TimerData) { 95 | td.fn = nil 96 | td.next = t.free 97 | t.free = td 98 | } 99 | 100 | // Add add the element x onto the heap. The complexity is 101 | // O(log(n)) where n = h.Len(). 102 | func (t *Timer) Add(expire itime.Duration, fn func()) (td *TimerData) { 103 | t.lock.Lock() 104 | td = t.get() 105 | td.expire = itime.Now().Add(expire) 106 | td.fn = fn 107 | t.add(td) 108 | t.lock.Unlock() 109 | return 110 | } 111 | 112 | // Del removes the element at index i from the heap. 113 | // The complexity is O(log(n)) where n = h.Len(). 114 | func (t *Timer) Del(td *TimerData) { 115 | t.lock.Lock() 116 | t.del(td) 117 | t.put(td) 118 | t.lock.Unlock() 119 | } 120 | 121 | // Push pushes the element x onto the heap. The complexity is 122 | // O(log(n)) where n = h.Len(). 123 | func (t *Timer) add(td *TimerData) { 124 | var d itime.Duration 125 | td.index = len(t.timers) 126 | // add to the minheap last node 127 | t.timers = append(t.timers, td) 128 | t.up(td.index) 129 | if td.index == 0 { 130 | // if first node, signal start goroutine 131 | d = td.Delay() 132 | t.signal.Reset(d) 133 | if Debug { 134 | log.Infof("timer: add reset delay %d ms", int64(d)/int64(itime.Millisecond)) 135 | } 136 | } 137 | if Debug { 138 | log.Infof("timer: push item key: %s, expire: %s, index: %d", td.Key, td.ExpireString(), td.index) 139 | } 140 | } 141 | 142 | func (t *Timer) del(td *TimerData) { 143 | var ( 144 | i = td.index 145 | last = len(t.timers) - 1 146 | ) 147 | if i < 0 || i > last || t.timers[i] != td { 148 | // already remove, usually by expire 149 | if Debug { 150 | log.Infof("timer del i: %d, last: %d, %p", i, last, td) 151 | } 152 | return 153 | } 154 | if i != last { 155 | t.swap(i, last) 156 | t.down(i, last) 157 | t.up(i) 158 | } 159 | // remove item is the last node 160 | t.timers[last].index = -1 // for safety 161 | t.timers = t.timers[:last] 162 | if Debug { 163 | log.Infof("timer: remove item key: %s, expire: %s, index: %d", td.Key, td.ExpireString(), td.index) 164 | } 165 | } 166 | 167 | // Set update timer data. 168 | func (t *Timer) Set(td *TimerData, expire itime.Duration) { 169 | t.lock.Lock() 170 | t.del(td) 171 | td.expire = itime.Now().Add(expire) 172 | t.add(td) 173 | t.lock.Unlock() 174 | } 175 | 176 | // start start the timer. 177 | func (t *Timer) start() { 178 | for { 179 | t.expire() 180 | <-t.signal.C 181 | } 182 | } 183 | 184 | // expire removes the minimum element (according to Less) from the heap. 185 | // The complexity is O(log(n)) where n = max. 186 | // It is equivalent to Del(0). 187 | func (t *Timer) expire() { 188 | var ( 189 | fn func() 190 | td *TimerData 191 | d itime.Duration 192 | ) 193 | t.lock.Lock() 194 | for { 195 | if len(t.timers) == 0 { 196 | d = infiniteDuration 197 | if Debug { 198 | log.Info("timer: no other instance") 199 | } 200 | break 201 | } 202 | td = t.timers[0] 203 | if d = td.Delay(); d > 0 { 204 | break 205 | } 206 | fn = td.fn 207 | // let caller put back 208 | t.del(td) 209 | t.lock.Unlock() 210 | if fn == nil { 211 | log.Warning("expire timer no fn") 212 | } else { 213 | if Debug { 214 | log.Infof("timer key: %s, expire: %s, index: %d expired, call fn", td.Key, td.ExpireString(), td.index) 215 | } 216 | fn() 217 | } 218 | t.lock.Lock() 219 | } 220 | t.signal.Reset(d) 221 | if Debug { 222 | log.Infof("timer: expier reset delay %d ms", int64(d)/int64(itime.Millisecond)) 223 | } 224 | t.lock.Unlock() 225 | } 226 | 227 | func (t *Timer) up(j int) { 228 | for { 229 | i := (j - 1) / 2 // parent 230 | if i >= j || !t.less(j, i) { 231 | break 232 | } 233 | t.swap(i, j) 234 | j = i 235 | } 236 | } 237 | 238 | func (t *Timer) down(i, n int) { 239 | for { 240 | j1 := 2*i + 1 241 | if j1 >= n || j1 < 0 { // j1 < 0 after int overflow 242 | break 243 | } 244 | j := j1 // left child 245 | if j2 := j1 + 1; j2 < n && !t.less(j1, j2) { 246 | j = j2 // = 2*i + 2 // right child 247 | } 248 | if !t.less(j, i) { 249 | break 250 | } 251 | t.swap(i, j) 252 | i = j 253 | } 254 | } 255 | 256 | func (t *Timer) less(i, j int) bool { 257 | return t.timers[i].expire.Before(t.timers[j].expire) 258 | } 259 | 260 | func (t *Timer) swap(i, j int) { 261 | t.timers[i], t.timers[j] = t.timers[j], t.timers[i] 262 | t.timers[i].index = i 263 | t.timers[j].index = j 264 | } 265 | -------------------------------------------------------------------------------- /pkg/time/timer_test.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | log "github.com/golang/glog" 8 | ) 9 | 10 | func TestTimer(t *testing.T) { 11 | timer := NewTimer(100) 12 | tds := make([]*TimerData, 100) 13 | for i := 0; i < 100; i++ { 14 | tds[i] = timer.Add(time.Duration(i)*time.Second+5*time.Minute, nil) 15 | } 16 | printTimer(timer) 17 | for i := 0; i < 100; i++ { 18 | log.Infof("td: %s, %s, %d", tds[i].Key, tds[i].ExpireString(), tds[i].index) 19 | timer.Del(tds[i]) 20 | } 21 | printTimer(timer) 22 | for i := 0; i < 100; i++ { 23 | tds[i] = timer.Add(time.Duration(i)*time.Second+5*time.Minute, nil) 24 | } 25 | printTimer(timer) 26 | for i := 0; i < 100; i++ { 27 | timer.Del(tds[i]) 28 | } 29 | printTimer(timer) 30 | timer.Add(time.Second, nil) 31 | time.Sleep(time.Second * 2) 32 | if len(timer.timers) != 0 { 33 | t.FailNow() 34 | } 35 | } 36 | 37 | func printTimer(timer *Timer) { 38 | log.Infof("----------timers: %d ----------", len(timer.timers)) 39 | for i := 0; i < len(timer.timers); i++ { 40 | log.Infof("timer: %s, %s, index: %d", timer.timers[i].Key, timer.timers[i].ExpireString(), timer.timers[i].index) 41 | } 42 | log.Infof("--------------------") 43 | } 44 | -------------------------------------------------------------------------------- /pkg/websocket/conn.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/Terry-Mao/goim/pkg/bufio" 10 | ) 11 | 12 | const ( 13 | // Frame header byte 0 bits from Section 5.2 of RFC 6455 14 | finBit = 1 << 7 15 | rsv1Bit = 1 << 6 16 | rsv2Bit = 1 << 5 17 | rsv3Bit = 1 << 4 18 | opBit = 0x0f 19 | 20 | // Frame header byte 1 bits from Section 5.2 of RFC 6455 21 | maskBit = 1 << 7 22 | lenBit = 0x7f 23 | 24 | continuationFrame = 0 25 | continuationFrameMaxRead = 100 26 | ) 27 | 28 | // The message types are defined in RFC 6455, section 11.8. 29 | const ( 30 | // TextMessage denotes a text data message. The text message payload is 31 | // interpreted as UTF-8 encoded text data. 32 | TextMessage = 1 33 | 34 | // BinaryMessage denotes a binary data message. 35 | BinaryMessage = 2 36 | 37 | // CloseMessage denotes a close control message. The optional message 38 | // payload contains a numeric code and text. Use the FormatCloseMessage 39 | // function to format a close message payload. 40 | CloseMessage = 8 41 | 42 | // PingMessage denotes a ping control message. The optional message payload 43 | // is UTF-8 encoded text. 44 | PingMessage = 9 45 | 46 | // PongMessage denotes a ping control message. The optional message payload 47 | // is UTF-8 encoded text. 48 | PongMessage = 10 49 | ) 50 | 51 | var ( 52 | // ErrMessageClose close control message 53 | ErrMessageClose = errors.New("close control message") 54 | // ErrMessageMaxRead continuation frame max read 55 | ErrMessageMaxRead = errors.New("continuation frame max read") 56 | ) 57 | 58 | // Conn represents a WebSocket connection. 59 | type Conn struct { 60 | rwc io.ReadWriteCloser 61 | r *bufio.Reader 62 | w *bufio.Writer 63 | maskKey []byte 64 | } 65 | 66 | // new connection 67 | func newConn(rwc io.ReadWriteCloser, r *bufio.Reader, w *bufio.Writer) *Conn { 68 | return &Conn{rwc: rwc, r: r, w: w, maskKey: make([]byte, 4)} 69 | } 70 | 71 | // WriteMessage write a message by type. 72 | func (c *Conn) WriteMessage(msgType int, msg []byte) (err error) { 73 | if err = c.WriteHeader(msgType, len(msg)); err != nil { 74 | return 75 | } 76 | err = c.WriteBody(msg) 77 | return 78 | } 79 | 80 | // WriteHeader write header frame. 81 | func (c *Conn) WriteHeader(msgType int, length int) (err error) { 82 | var h []byte 83 | if h, err = c.w.Peek(2); err != nil { 84 | return 85 | } 86 | // 1.First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) 87 | h[0] = 0 88 | h[0] |= finBit | byte(msgType) 89 | // 2.Second byte. Mask/Payload len(7bits) 90 | h[1] = 0 91 | switch { 92 | case length <= 125: 93 | // 7 bits 94 | h[1] |= byte(length) 95 | case length < 65536: 96 | // 16 bits 97 | h[1] |= 126 98 | if h, err = c.w.Peek(2); err != nil { 99 | return 100 | } 101 | binary.BigEndian.PutUint16(h, uint16(length)) 102 | default: 103 | // 64 bits 104 | h[1] |= 127 105 | if h, err = c.w.Peek(8); err != nil { 106 | return 107 | } 108 | binary.BigEndian.PutUint64(h, uint64(length)) 109 | } 110 | return 111 | } 112 | 113 | // WriteBody write a message body. 114 | func (c *Conn) WriteBody(b []byte) (err error) { 115 | if len(b) > 0 { 116 | _, err = c.w.Write(b) 117 | } 118 | return 119 | } 120 | 121 | // Peek write peek. 122 | func (c *Conn) Peek(n int) ([]byte, error) { 123 | return c.w.Peek(n) 124 | } 125 | 126 | // Flush flush writer buffer 127 | func (c *Conn) Flush() error { 128 | return c.w.Flush() 129 | } 130 | 131 | // ReadMessage read a message. 132 | func (c *Conn) ReadMessage() (op int, payload []byte, err error) { 133 | var ( 134 | fin bool 135 | finOp, n int 136 | partPayload []byte 137 | ) 138 | for { 139 | // read frame 140 | if fin, op, partPayload, err = c.readFrame(); err != nil { 141 | return 142 | } 143 | switch op { 144 | case BinaryMessage, TextMessage, continuationFrame: 145 | if fin && len(payload) == 0 { 146 | return op, partPayload, nil 147 | } 148 | // continuation frame 149 | payload = append(payload, partPayload...) 150 | if op != continuationFrame { 151 | finOp = op 152 | } 153 | // final frame 154 | if fin { 155 | op = finOp 156 | return 157 | } 158 | case PingMessage: 159 | // handler ping 160 | if err = c.WriteMessage(PongMessage, partPayload); err != nil { 161 | return 162 | } 163 | case PongMessage: 164 | // handler pong 165 | case CloseMessage: 166 | // handler close 167 | err = ErrMessageClose 168 | return 169 | default: 170 | err = fmt.Errorf("unknown control message, fin=%t, op=%d", fin, op) 171 | return 172 | } 173 | if n > continuationFrameMaxRead { 174 | err = ErrMessageMaxRead 175 | return 176 | } 177 | n++ 178 | } 179 | } 180 | 181 | func (c *Conn) readFrame() (fin bool, op int, payload []byte, err error) { 182 | var ( 183 | b byte 184 | p []byte 185 | mask bool 186 | maskKey []byte 187 | payloadLen int64 188 | ) 189 | // 1.First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) 190 | b, err = c.r.ReadByte() 191 | if err != nil { 192 | return 193 | } 194 | // final frame 195 | fin = (b & finBit) != 0 196 | // rsv MUST be 0 197 | if rsv := b & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { 198 | return false, 0, nil, fmt.Errorf("unexpected reserved bits rsv1=%d, rsv2=%d, rsv3=%d", b&rsv1Bit, b&rsv2Bit, b&rsv3Bit) 199 | } 200 | // op code 201 | op = int(b & opBit) 202 | // 2.Second byte. Mask/Payload len(7bits) 203 | b, err = c.r.ReadByte() 204 | if err != nil { 205 | return 206 | } 207 | // is mask payload 208 | mask = (b & maskBit) != 0 209 | // payload length 210 | switch b & lenBit { 211 | case 126: 212 | // 16 bits 213 | if p, err = c.r.Pop(2); err != nil { 214 | return 215 | } 216 | payloadLen = int64(binary.BigEndian.Uint16(p)) 217 | case 127: 218 | // 64 bits 219 | if p, err = c.r.Pop(8); err != nil { 220 | return 221 | } 222 | payloadLen = int64(binary.BigEndian.Uint64(p)) 223 | default: 224 | // 7 bits 225 | payloadLen = int64(b & lenBit) 226 | } 227 | // read mask key 228 | if mask { 229 | maskKey, err = c.r.Pop(4) 230 | if err != nil { 231 | return 232 | } 233 | if c.maskKey == nil { 234 | c.maskKey = make([]byte, 4) 235 | } 236 | copy(c.maskKey, maskKey) 237 | } 238 | // read payload 239 | if payloadLen > 0 { 240 | if payload, err = c.r.Pop(int(payloadLen)); err != nil { 241 | return 242 | } 243 | if mask { 244 | maskBytes(c.maskKey, 0, payload) 245 | } 246 | } 247 | return 248 | } 249 | 250 | // Close close the connection. 251 | func (c *Conn) Close() error { 252 | return c.rwc.Close() 253 | } 254 | 255 | func maskBytes(key []byte, pos int, b []byte) int { 256 | for i := range b { 257 | b[i] ^= key[pos&3] 258 | pos++ 259 | } 260 | return pos & 3 261 | } 262 | -------------------------------------------------------------------------------- /pkg/websocket/request.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/Terry-Mao/goim/pkg/bufio" 10 | ) 11 | 12 | // Request request. 13 | type Request struct { 14 | Method string 15 | RequestURI string 16 | Proto string 17 | Host string 18 | Header http.Header 19 | 20 | reader *bufio.Reader 21 | } 22 | 23 | // ReadRequest reads and parses an incoming request from b. 24 | func ReadRequest(r *bufio.Reader) (req *Request, err error) { 25 | var ( 26 | b []byte 27 | ok bool 28 | ) 29 | req = &Request{reader: r} 30 | if b, err = req.readLine(); err != nil { 31 | return 32 | } 33 | if req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(string(b)); !ok { 34 | return nil, fmt.Errorf("malformed HTTP request %s", b) 35 | } 36 | if req.Header, err = req.readMIMEHeader(); err != nil { 37 | return 38 | } 39 | req.Host = req.Header.Get("Host") 40 | return req, nil 41 | } 42 | 43 | func (r *Request) readLine() ([]byte, error) { 44 | var line []byte 45 | for { 46 | l, more, err := r.reader.ReadLine() 47 | if err != nil { 48 | return nil, err 49 | } 50 | // Avoid the copy if the first call produced a full line. 51 | if line == nil && !more { 52 | return l, nil 53 | } 54 | line = append(line, l...) 55 | if !more { 56 | break 57 | } 58 | } 59 | return line, nil 60 | } 61 | 62 | func (r *Request) readMIMEHeader() (header http.Header, err error) { 63 | var ( 64 | line []byte 65 | i int 66 | k, v string 67 | ) 68 | header = make(http.Header, 16) 69 | for { 70 | if line, err = r.readLine(); err != nil { 71 | return 72 | } 73 | line = trim(line) 74 | if len(line) == 0 { 75 | return 76 | } 77 | if i = bytes.IndexByte(line, ':'); i <= 0 { 78 | err = fmt.Errorf("malformed MIME header line: " + string(line)) 79 | return 80 | } 81 | k = string(line[:i]) 82 | // Skip initial spaces in value. 83 | i++ // skip colon 84 | for i < len(line) && (line[i] == ' ' || line[i] == '\t') { 85 | i++ 86 | } 87 | v = string(line[i:]) 88 | header.Add(k, v) 89 | } 90 | } 91 | 92 | // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts. 93 | func parseRequestLine(line string) (method, requestURI, proto string, ok bool) { 94 | s1 := strings.Index(line, " ") 95 | s2 := strings.Index(line[s1+1:], " ") 96 | if s1 < 0 || s2 < 0 { 97 | return 98 | } 99 | s2 += s1 + 1 100 | return line[:s1], line[s1+1 : s2], line[s2+1:], true 101 | } 102 | 103 | // trim returns s with leading and trailing spaces and tabs removed. 104 | // It does not assume Unicode or UTF-8. 105 | func trim(s []byte) []byte { 106 | i := 0 107 | for i < len(s) && (s[i] == ' ' || s[i] == '\t') { 108 | i++ 109 | } 110 | n := len(s) 111 | for n > i && (s[n-1] == ' ' || s[n-1] == '\t') { 112 | n-- 113 | } 114 | return s[i:n] 115 | } 116 | -------------------------------------------------------------------------------- /pkg/websocket/server.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "errors" 7 | "io" 8 | "strings" 9 | 10 | "github.com/Terry-Mao/goim/pkg/bufio" 11 | ) 12 | 13 | var ( 14 | keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") 15 | // ErrBadRequestMethod bad request method 16 | ErrBadRequestMethod = errors.New("bad method") 17 | // ErrNotWebSocket not websocket protocal 18 | ErrNotWebSocket = errors.New("not websocket protocol") 19 | // ErrBadWebSocketVersion bad websocket version 20 | ErrBadWebSocketVersion = errors.New("missing or bad WebSocket Version") 21 | // ErrChallengeResponse mismatch challenge response 22 | ErrChallengeResponse = errors.New("mismatch challenge/response") 23 | ) 24 | 25 | // Upgrade Switching Protocols 26 | func Upgrade(rwc io.ReadWriteCloser, rr *bufio.Reader, wr *bufio.Writer, req *Request) (conn *Conn, err error) { 27 | if req.Method != "GET" { 28 | return nil, ErrBadRequestMethod 29 | } 30 | if req.Header.Get("Sec-Websocket-Version") != "13" { 31 | return nil, ErrBadWebSocketVersion 32 | } 33 | if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" { 34 | return nil, ErrNotWebSocket 35 | } 36 | if !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { 37 | return nil, ErrNotWebSocket 38 | } 39 | challengeKey := req.Header.Get("Sec-Websocket-Key") 40 | if challengeKey == "" { 41 | return nil, ErrChallengeResponse 42 | } 43 | _, _ = wr.WriteString("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n") 44 | _, _ = wr.WriteString("Sec-WebSocket-Accept: " + computeAcceptKey(challengeKey) + "\r\n\r\n") 45 | if err = wr.Flush(); err != nil { 46 | return 47 | } 48 | return newConn(rwc, rr, wr), nil 49 | } 50 | 51 | func computeAcceptKey(challengeKey string) string { 52 | h := sha1.New() 53 | _, _ = h.Write([]byte(challengeKey)) 54 | _, _ = h.Write(keyGUID) 55 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/websocket/server_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "golang.org/x/net/websocket" 10 | 11 | "github.com/Terry-Mao/goim/pkg/bufio" 12 | ) 13 | 14 | func TestServer(t *testing.T) { 15 | var ( 16 | data = []byte{0, 1, 2} 17 | ) 18 | ln, err := net.Listen("tcp", ":8080") 19 | if err != nil { 20 | t.FailNow() 21 | } 22 | go func() { 23 | conn, err := ln.Accept() 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | rd := bufio.NewReader(conn) 28 | wr := bufio.NewWriter(conn) 29 | req, err := ReadRequest(rd) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | if req.RequestURI != "/sub" { 34 | t.Error(err) 35 | } 36 | ws, err := Upgrade(conn, rd, wr, req) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | if err = ws.WriteMessage(BinaryMessage, data); err != nil { 41 | t.Error(err) 42 | } 43 | if err = ws.Flush(); err != nil { 44 | t.Error(err) 45 | } 46 | op, b, err := ws.ReadMessage() 47 | if err != nil || op != BinaryMessage || !reflect.DeepEqual(b, data) { 48 | t.Error(err) 49 | } 50 | }() 51 | time.Sleep(time.Millisecond * 100) 52 | // ws client 53 | ws, err := websocket.Dial("ws://127.0.0.1:8080/sub", "", "*") 54 | if err != nil { 55 | t.FailNow() 56 | } 57 | // receive binary frame 58 | var b []byte 59 | if err = websocket.Message.Receive(ws, &b); err != nil { 60 | t.FailNow() 61 | } 62 | if !reflect.DeepEqual(b, data) { 63 | t.FailNow() 64 | } 65 | // send binary frame 66 | if err = websocket.Message.Send(ws, data); err != nil { 67 | t.FailNow() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Installing 2 | 3 | ### Install JDK 4 | ``` 5 | ./jdk.sh 6 | ``` 7 | 8 | ### Install Zookeeper 9 | ``` 10 | ./zk.sh 11 | ``` 12 | 13 | ### Install Kafka 14 | ``` 15 | ./kafka.sh 16 | ``` 17 | -------------------------------------------------------------------------------- /scripts/jdk8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # config parameters 3 | JDK_HOME=/usr/local/jdk8/ 4 | # download jdk 5 | curl -L https://download.oracle.com/otn-pub/java/jdk/8u191-b12/2787e4a523244c269598db4e85c51e0c/jdk-8u191-linux-x64.tar.gz -o jdk-8u191-linux-x64.tar.gz 6 | tar zxf jdk-8u191-linux-x64.tar.gz 7 | # install jdk 8 | mkdir -p $JDK_HOME 9 | mv jdk1.8.0/* $JDK_HOME 10 | update-alternatives --install "/usr/bin/java" "java" "$JDK_HOME/bin/java" 1500 11 | update-alternatives --install "/usr/bin/javac" "javac" "$JDK_HOME/bin/javac" 1500 12 | update-alternatives --install "/usr/bin/javaws" "javaws" "$JDK_HOME/bin/javaws" 1500 13 | -------------------------------------------------------------------------------- /scripts/kafka.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # config parameters 3 | JDK_HOME=/usr/local/jdk8/ 4 | KAFKA_HOME=/data/app/kafka/ 5 | KAFKA_CONF=/data/app/kafka/config/server.properties 6 | KAFKA_DATA=/data/kafka_data/ 7 | KAFKA_SUPERVISOR=/etc/supervisor/conf.d/kafka.conf 8 | 9 | USAGE="./kafka.sh {broker.id} {current.addr} {zk.addr1,zk.addr2,zk.addr3}\r\neg: ./kafka.sh 1 127.0.0.1 127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181" 10 | # check parammeters 11 | BROKER_ID=$1 12 | CURRENT_ADDR=$2 13 | ZK_ADDRS=$3 14 | if [ ! "$BROKER_ID" ]; then 15 | echo "kafka {broker.id} can not be null" 16 | echo -e $USAGE 17 | exit 1 18 | fi 19 | if [ ! "$CURRENT_ADDR" ]; then 20 | echo "current addr can not be null" 21 | exit 1 22 | fi 23 | if [ ! "$ZK_ADDRS" ]; then 24 | echo "zookeeper addrs can not be null" 25 | exit 1 26 | fi 27 | if [ ! -d "$JDK_HOME" ]; then 28 | echo "jdk not exist down jdk8" 29 | ./jdk8.sh 30 | fi 31 | 32 | # download kafka 33 | curl "http://apache.stu.edu.tw/kafka/2.1.0/kafka_2.11-2.1.0.tgz" -o kafka_2.11-2.1.0.tgz 34 | tar zxf kafka_2.11-2.1.0.tgz 35 | # install kafka 36 | mkdir -p $KAFKA_HOME 37 | mkdir -p $KAFKA_DATA 38 | mv kafka_2.11-2.1.0/* $KAFKA_HOME 39 | # kafka config 40 | echo "broker.id=$BROKER_ID">$KAFKA_CONF 41 | echo "host.name=$CURRENT_ADDR">>$KAFKA_CONF 42 | echo "zookeeper.connect=$ZK_ADDRS/kafka">>$KAFKA_CONF 43 | echo "num.network.threads=4 44 | num.io.threads=4 45 | socket.send.buffer.bytes=1024000 46 | socket.receive.buffer.bytes=1024000 47 | socket.request.max.bytes=52428800 48 | log.dirs=$KAFKA_DATA 49 | num.partitions=9 50 | num.recovery.threads.per.data.dir=1 51 | log.cleanup.policy=delete 52 | log.retention.hours=24 53 | log.segment.bytes=536870912 54 | log.retention.check.interval.ms=300000 55 | log.cleaner.enable=false 56 | zookeeper.connection.timeout.ms=6000 57 | default.replication.factor=2 58 | delete.topic.enable=false 59 | auto.create.topics.enable=false">>$KAFKA_CONF 60 | 61 | # install supervisor 62 | if ! type "supervisorctl" > /dev/null; then 63 | apt-get install -y supervisor 64 | fi 65 | # kafka supervisor config 66 | mkdir -p /etc/supervisor/conf.d/ 67 | mkdir -p /data/log/kafka/ 68 | echo "[program:kafka] 69 | command=$KAFKA_HOME/bin/kafka-server-start.sh $KAFKA_CONF 70 | user=root 71 | autostart=true 72 | autorestart=true 73 | exitcodes=0 74 | startsecs=10 75 | startretries=10 76 | stopwaitsecs=10 77 | stopsignal=KILL 78 | stdout_logfile=/data/log/kafka/stdout.log 79 | stderr_logfile=/data/log/kafka/stderr.log 80 | stdout_logfile_maxbytes=100MB 81 | stdout_logfile_backups=5 82 | stderr_logfile_maxbytes=100MB 83 | stderr_logfile_backups=5 84 | environment=JAVA_HOME=$JDK_HOME,JRE_HOME='$JDK_HOME/jre',KAFKA_HEAP_OPTS='-Xmx6g -Xms6g -XX:MetaspaceSize=96m -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80'">$_KAFKA_SUPERVISOR 85 | 86 | supervisorctl update 87 | -------------------------------------------------------------------------------- /scripts/zk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # config parameters 3 | JDK_HOME=/usr/local/jdk8 4 | ZK_HOME=/data/server/zookeeper 5 | ZK_SUPERVISOR=/etc/supervisor/conf.d/zookeeper.conf 6 | 7 | USAGE="./zk.sh {zk.id} {zk.addr1,zk.addr2,zk.addr3}\r\neg:./zk.sh 1 127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181" 8 | # check parammeters 9 | ZK_ID=$1 10 | str=$2 11 | ZK_ADDRS=(${str//,/ }) 12 | ZK_ADDRS_SIZE=${#ZK_ADDRS[@]} 13 | if [ ! -n "$1" ];then 14 | echo -e $USAGE 15 | exit 1 16 | fi 17 | if [ ! -n "$2" ];then 18 | echo -e $USAGE 19 | exit 1 20 | fi 21 | echo $ZK_ID 22 | echo ${ZK_ADDRS[@]} 23 | 24 | 25 | set -e 26 | 27 | curl -L http://apache.website-solution.net/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz -o zookeeper-3.4.12.tar.gz 28 | tar zxf zookeeper-3.4.12.tar.gz 29 | mkdir -p $ZK_HOME 30 | echo $ZK_ID>$ZK_HOME/myid 31 | mv zookeeper-3.4.12/* $ZK_HOME 32 | echo "tickTime=2000" > $ZK_HOME/conf/zoo.cfg 33 | echo "initLimit=10" >> $ZK_HOME/conf/zoo.cfg 34 | echo "syncLimit=5" >> $ZK_HOME/conf/zoo.cfg 35 | echo "dataDir=/data/server/zookeeper" >> $ZK_HOME/conf/zoo.cfg 36 | echo "clientPort=2181" >> $ZK_HOME/conf/zoo.cfg 37 | echo "autopurge.snapRetainCount=5" >> $ZK_HOME/conf/zoo.cfg 38 | echo "autopurge.purgeInterval=24" >> $ZK_HOME/conf/zoo.cfg 39 | for ((index=0;index<$ZK_ADDRS_SIZE;index++)) 40 | do 41 | id=$[$index+1] 42 | echo "server.$id=${ZK_ADDRS[index]}:2888:3888" >> $ZK_HOME/conf/zoo.cfg 43 | done 44 | 45 | # install supervisor 46 | if ! type "supervisorctl" > /dev/null; then 47 | apt-get install -y supervisor 48 | fi 49 | # zookeeper supervisor config 50 | mkdir -p /etc/supervisor/conf.d/ 51 | mkdir -p /data/log/zookeeper/ 52 | echo "[program:zookeeper] 53 | command=${ZK_HOME}/bin/zkServer.sh start-foreground 54 | directory=${ZK_HOME} 55 | user=root 56 | autostart=true 57 | autorestart=true 58 | exitcodes=0 59 | startsecs=10 60 | startretries=10 61 | stopwaitsecs=10 62 | stopsignal=KILL 63 | stdout_logfile=/data/log/zookeeper/stdout.log 64 | stderr_logfile=/data/log/zookeeper/stderr.log 65 | stdout_logfile_maxbytes=100MB 66 | stdout_logfile_backups=5 67 | stderr_logfile_maxbytes=100MB 68 | stderr_logfile_backups=5 69 | environment=JAVA_HOME=$JDK_HOME,JRE_HOME='$JDK_HOME/jre' 70 | ">$ZK_SUPERVISOR 71 | 72 | supervisorctl update 73 | --------------------------------------------------------------------------------