├── .gitignore ├── E0B82999-82BB-4963-BC2D-FDEAF42230FC.jpeg ├── README.md ├── bandwidth.png ├── common ├── Errors.go └── Protocol.go ├── cpu.png ├── gateway ├── Bucket.go ├── Config.go ├── ConnMgr.go ├── Merger.go ├── Room.go ├── Service.go ├── Stats.go ├── WSConnection.go ├── WSHandler.go ├── WSServer.go ├── cli │ ├── client.html │ ├── default.key │ ├── default.pem │ ├── gateway.json │ └── main.go └── test │ ├── client.go │ └── stats.go ├── go.mod ├── go.sum ├── logic ├── Config.go ├── GateConn.go ├── GateConnMgr.go ├── Service.go ├── Stats.go └── cli │ ├── logic.json │ └── main.go └── xingqiu.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /E0B82999-82BB-4963-BC2D-FDEAF42230FC.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owenliang/go-push/b5b6a2e1cf9172762800f263198b65058a99cbe9/E0B82999-82BB-4963-BC2D-FDEAF42230FC.jpeg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-push 2 | 3 | golang实现的、可扩展的通用消息推送原型。 4 | 5 | # 安装 6 | 7 | 已升级到golang1.13,基于gomod管理依赖。 8 | 9 | * 下载go-push 10 | 11 | ``` 12 | go get github.com/owenliang/go-push 13 | ``` 14 | 15 | * 安装依赖 16 | 17 | ``` 18 | export GOPROXY=goproxy.io 19 | go mod download 20 | ``` 21 | 22 | * 编译gateway服务 23 | 24 | ``` 25 | cd gateway/cli && go build && cd - 26 | ``` 27 | 28 | * 编译logic服务 29 | 30 | ``` 31 | cd logic/cli && go build && cd - 32 | ``` 33 | 34 | # 架构 35 | 36 | * gateway: 长连接网关 37 | * 海量长连接按BUCKET打散, 减小推送遍历的锁粒度 38 | * 按广播/房间粒度的消息前置合并, 减少编码CPU损耗, 减少系统网络调用, 巨幅提升吞吐 39 | * logic: 逻辑服务器 40 | * 本身无状态, 负责将推送消息分发到所有gateway节点 41 | * 对调用方暴露HTTP/1接口, 方便业务对接 42 | * 采用HTTP/2长连接RPC向gateway集群分发消息 43 | 44 | # 潜在问题 45 | 46 | * 推送主要瓶颈是gateway层而不是内部通讯, 所以gateway和logic之间仍旧采用了小包通讯(对网卡有PPS压力), 同时logic为业务提供了批量推送接口来缓解特殊需求. 47 | 48 | # 压测 49 | 50 | ## 环境 51 | 52 | * 16 vcore 53 | * client, logic, gateway deployed together 54 | 55 | ## 带宽 56 | 57 |  58 | 59 | ## CPU占用 60 | 61 |  62 | 63 | # logic的推送API 64 | 65 | * 全员广播 66 | 67 | ``` 68 | curl http://localhost:7799/push/all -d 'items=[{"msg": "hi"},{"msg": "bye"}]' 69 | ``` 70 | 71 | * 房间广播 72 | 73 | ``` 74 | curl http://localhost:7799/push/room -d 'room=default&items=[{"msg": "hi"},{"msg": "bye"}]' 75 | ``` 76 | 77 | ## gateway的websocekt协议 78 | 79 | * PING(客户端->服务端) 80 | 81 | ``` 82 | {"type": "PING", "data": {}} 83 | ``` 84 | 85 | * PONG(服务端->客户端) 86 | 87 | ``` 88 | {"type": "PONG", "data": {}} 89 | ``` 90 | 91 | * JOIN(客户端->服务端) 92 | 93 | ``` 94 | {"type": "JOIN", "data": {"room": "fengtimo"}} 95 | ``` 96 | 97 | * LEAVE(客户端->服务端) 98 | 99 | ``` 100 | {"type": "LEAVE", "data": {"room": "fengtimo"}} 101 | ``` 102 | 103 | * PUSH(服务端->客户端) 104 | 105 | ``` 106 | {"type": "PUSH", "data": {"items": [{"name": "go-push"}, {"age": "1"}]}} 107 | ``` 108 | 109 | # 加入社群 110 | 111 | 我的博客: 112 | 113 | [鱼儿的博客](https://yuerblog.cc) 114 | 115 | Go微信群: 116 | 117 |  118 | 119 | 知识星球(独家知识): 120 |  -------------------------------------------------------------------------------- /bandwidth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owenliang/go-push/b5b6a2e1cf9172762800f263198b65058a99cbe9/bandwidth.png -------------------------------------------------------------------------------- /common/Errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ( 6 | ERR_CONNECTION_LOSS = errors.New("ERR_CONNECTION_LOSS") 7 | 8 | ERR_SEND_MESSAGE_FULL = errors.New("ERR_SEND_MESSAGE_FULL") 9 | 10 | ERR_JOIN_ROOM_TWICE = errors.New("ERR_JOIN_ROOM_TWICE") 11 | 12 | ERR_NOT_IN_ROOM = errors.New("ERR_NOT_IN_ROOM") 13 | 14 | ERR_ROOM_ID_INVALID = errors.New("ERR_ROOM_ID_INVALID") 15 | 16 | ERR_DISPATCH_CHANNEL_FULL = errors.New("ERR_DISPATCH_CHANNEL_FULL") 17 | 18 | ERR_MERGE_CHANNEL_FULL = errors.New("ERR_MERGE_CHANNEL_FULL") 19 | 20 | ERR_CERT_INVALID = errors.New("ERR_CERT_INVALID") 21 | 22 | ERR_LOGIC_DISPATCH_CHANNEL_FULL = errors.New("ERR_LOGIC_DISPATCH_CHANNEL_FULL") 23 | ) 24 | -------------------------------------------------------------------------------- /common/Protocol.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gorilla/websocket" 6 | ) 7 | 8 | // 推送类型 9 | const ( 10 | PUSH_TYPE_ROOM = 1 // 推送房间 11 | PUSH_TYPE_ALL = 2 // 推送在线 12 | ) 13 | 14 | // websocket的Message对象 15 | type WSMessage struct { 16 | MsgType int 17 | MsgData []byte 18 | } 19 | 20 | // 业务消息的固定格式(type+data) 21 | type BizMessage struct { 22 | Type string `json:"type"` // type消息类型: PING, PONG, JOIN, LEAVE, PUSH 23 | Data json.RawMessage `json:"data"` // data数据字段 24 | } 25 | 26 | // Data数据类型 27 | 28 | // PUSH 29 | type BizPushData struct { 30 | Items []*json.RawMessage `json:"items"` 31 | } 32 | 33 | // PING 34 | type BizPingData struct {} 35 | 36 | // PONG 37 | type BizPongData struct {} 38 | 39 | // JOIN 40 | type BizJoinData struct { 41 | Room string `json:"room"` 42 | } 43 | 44 | // LEAVE 45 | type BizLeaveData struct { 46 | Room string `json:"room"` 47 | } 48 | 49 | func BuildWSMessage(msgType int, msgData []byte) (wsMessage *WSMessage) { 50 | return &WSMessage{ 51 | MsgType: msgType, 52 | MsgData: msgData, 53 | } 54 | } 55 | 56 | func EncodeWSMessage(bizMessage *BizMessage) (wsMessage *WSMessage, err error){ 57 | var ( 58 | buf []byte 59 | ) 60 | if buf, err = json.Marshal(*bizMessage); err != nil { 61 | return 62 | } 63 | wsMessage = &WSMessage{websocket.TextMessage, buf} 64 | return 65 | } 66 | 67 | // 解析{"type": "PING", "data": {...}}的包 68 | func DecodeBizMessage(buf []byte) (bizMessage *BizMessage, err error) { 69 | var ( 70 | bizMsgObj BizMessage 71 | ) 72 | 73 | if err = json.Unmarshal(buf, &bizMsgObj); err != nil { 74 | return 75 | } 76 | 77 | bizMessage = &bizMsgObj 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owenliang/go-push/b5b6a2e1cf9172762800f263198b65058a99cbe9/cpu.png -------------------------------------------------------------------------------- /gateway/Bucket.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "sync" 5 | "github.com/owenliang/go-push/common" 6 | ) 7 | 8 | type Bucket struct { 9 | rwMutex sync.RWMutex 10 | index int // 我是第几个桶 11 | id2Conn map[uint64]*WSConnection // 连接列表(key=连接唯一ID) 12 | rooms map[string]*Room // 房间列表 13 | } 14 | 15 | func InitBucket(bucketIdx int) (bucket *Bucket) { 16 | bucket = &Bucket{ 17 | index: bucketIdx, 18 | id2Conn: make(map[uint64]*WSConnection), 19 | rooms: make(map[string]*Room), 20 | } 21 | return 22 | } 23 | 24 | func (bucket *Bucket) AddConn(wsConn *WSConnection) { 25 | bucket.rwMutex.Lock() 26 | defer bucket.rwMutex.Unlock() 27 | 28 | bucket.id2Conn[wsConn.connId] = wsConn 29 | } 30 | 31 | func (bucket *Bucket) DelConn(wsConn *WSConnection) { 32 | bucket.rwMutex.Lock() 33 | defer bucket.rwMutex.Unlock() 34 | 35 | delete(bucket.id2Conn, wsConn.connId) 36 | } 37 | 38 | func (bucket *Bucket) JoinRoom(roomId string, wsConn *WSConnection) (err error) { 39 | var ( 40 | existed bool 41 | room *Room 42 | ) 43 | bucket.rwMutex.Lock() 44 | defer bucket.rwMutex.Unlock() 45 | 46 | // 找到房间 47 | if room, existed = bucket.rooms[roomId]; !existed { 48 | room = InitRoom(roomId) 49 | bucket.rooms[roomId] = room 50 | RoomCount_INCR() 51 | } 52 | // 加入房间 53 | err = room.Join(wsConn) 54 | return 55 | } 56 | 57 | func (bucket *Bucket) LeaveRoom(roomId string, wsConn *WSConnection) (err error) { 58 | var ( 59 | existed bool 60 | room *Room 61 | ) 62 | bucket.rwMutex.Lock() 63 | defer bucket.rwMutex.Unlock() 64 | 65 | // 找到房间 66 | if room, existed = bucket.rooms[roomId]; !existed { 67 | err = common.ERR_NOT_IN_ROOM 68 | return 69 | } 70 | 71 | err = room.Leave(wsConn) 72 | 73 | // 房间为空, 则删除 74 | if room.Count() == 0 { 75 | delete(bucket.rooms, roomId) 76 | RoomCount_DESC() 77 | } 78 | return 79 | } 80 | 81 | // 推送给Bucket内所有用户 82 | func (bucket *Bucket) PushAll(wsMsg *common.WSMessage) { 83 | var ( 84 | wsConn *WSConnection 85 | ) 86 | 87 | // 锁Bucket 88 | bucket.rwMutex.RLock() 89 | defer bucket.rwMutex.RUnlock() 90 | 91 | // 全量非阻塞推送 92 | for _, wsConn = range bucket.id2Conn { 93 | wsConn.SendMessage(wsMsg) 94 | } 95 | } 96 | 97 | // 推送给某个房间的所有用户 98 | func (bucket *Bucket) PushRoom(roomId string, wsMsg *common.WSMessage) { 99 | var ( 100 | room *Room 101 | existed bool 102 | ) 103 | 104 | // 锁Bucket 105 | bucket.rwMutex.RLock() 106 | room, existed = bucket.rooms[roomId] 107 | bucket.rwMutex.RUnlock() 108 | 109 | // 房间不存在 110 | if !existed { 111 | return 112 | } 113 | 114 | // 向房间做推送 115 | room.Push(wsMsg) 116 | } 117 | -------------------------------------------------------------------------------- /gateway/Config.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "io/ioutil" 5 | "encoding/json" 6 | ) 7 | 8 | // 程序配置 9 | type Config struct { 10 | WsPort int `json:"wsPort"` 11 | WsReadTimeout int `json:"wsReadTimeout"` 12 | WsWriteTimeout int `json:"wsWriteTimeout"` 13 | WsInChannelSize int `json:"wsInChannelSize"` 14 | WsOutChannelSize int `json:"wsOutChannelSize"` 15 | WsHeartbeatInterval int `json:"wsHeartbeatInterval"` 16 | MaxMergerDelay int `json:"maxMergerDelay"` 17 | MaxMergerBatchSize int `json:"maxMergerBatchSize"` 18 | MergerWorkerCount int `json:"mergerWorkerCount"` 19 | MergerChannelSize int `json:"mergerChannelSize"` 20 | ServicePort int `json:"servicePort"` 21 | ServiceReadTimeout int `json:"serviceReadTimeout"` 22 | ServiceWriteTimeout int `json:"serviceWriteTimeout"` 23 | ServerPem string `json:"serverPem"` 24 | ServerKey string `json:"serverKey"` 25 | BucketCount int `json:"bucketCount"` 26 | BucketWorkerCount int `json:"bucketWorkerCount"` 27 | MaxJoinRoom int`json:"maxJoinRoom"` 28 | DispatchChannelSize int `json:"dispatchChannelSize"` 29 | DispatchWorkerCount int `json:"dispatchWorkerCount"` 30 | BucketJobChannelSize int `json:"bucketJobChannelSize"` 31 | BucketJobWorkerCount int `json:"bucketJobWorkerCount"` 32 | } 33 | 34 | var ( 35 | G_config *Config 36 | ) 37 | 38 | func InitConfig(filename string) (err error) { 39 | var ( 40 | content []byte 41 | conf Config 42 | ) 43 | 44 | if content, err = ioutil.ReadFile(filename); err != nil { 45 | return 46 | } 47 | 48 | if err = json.Unmarshal(content, &conf); err != nil { 49 | return 50 | } 51 | 52 | G_config = &conf 53 | return 54 | } -------------------------------------------------------------------------------- /gateway/ConnMgr.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import "github.com/owenliang/go-push/common" 4 | 5 | // 推送任务 6 | type PushJob struct { 7 | pushType int // 推送类型 8 | roomId string // 房间ID 9 | // union { 10 | bizMsg *common.BizMessage // 未序列化的业务消息 11 | wsMsg *common.WSMessage // 已序列化的业务消息 12 | // } 13 | } 14 | 15 | // 连接管理器 16 | type ConnMgr struct { 17 | buckets []*Bucket 18 | jobChan []chan*PushJob // 每个Bucket对应一个Job Queue 19 | 20 | dispatchChan chan *PushJob // 待分发消息队列 21 | } 22 | 23 | var ( 24 | G_connMgr *ConnMgr 25 | ) 26 | 27 | // 消息分发到Bucket 28 | func (connMgr *ConnMgr)dispatchWorkerMain(dispatchWorkerIdx int) { 29 | var ( 30 | bucketIdx int 31 | pushJob *PushJob 32 | err error 33 | ) 34 | for { 35 | select { 36 | case pushJob = <- connMgr.dispatchChan: 37 | DispatchPending_DESC() 38 | 39 | // 序列化 40 | if pushJob.wsMsg, err = common.EncodeWSMessage(pushJob.bizMsg); err != nil { 41 | continue 42 | } 43 | // 分发给所有Bucket, 若Bucket拥塞则等待 44 | for bucketIdx, _ = range connMgr.buckets { 45 | PushJobPending_INCR() 46 | connMgr.jobChan[bucketIdx] <- pushJob 47 | } 48 | } 49 | } 50 | } 51 | 52 | // Job负责消息广播给客户端 53 | func (connMgr *ConnMgr)jobWorkerMain(jobWorkerIdx int, bucketIdx int) { 54 | var ( 55 | bucket = connMgr.buckets[bucketIdx] 56 | pushJob *PushJob 57 | ) 58 | 59 | for { 60 | select { 61 | case pushJob = <-connMgr.jobChan[bucketIdx]: // 从Bucket的job queue取出一个任务 62 | PushJobPending_DESC() 63 | if pushJob.pushType == common.PUSH_TYPE_ALL { 64 | bucket.PushAll(pushJob.wsMsg) 65 | } else if pushJob.pushType == common.PUSH_TYPE_ROOM { 66 | bucket.PushRoom(pushJob.roomId, pushJob.wsMsg) 67 | } 68 | } 69 | } 70 | } 71 | 72 | /** 73 | 以下是API 74 | */ 75 | 76 | func InitConnMgr() (err error) { 77 | var ( 78 | bucketIdx int 79 | jobWorkerIdx int 80 | dispatchWorkerIdx int 81 | connMgr *ConnMgr 82 | ) 83 | 84 | connMgr = &ConnMgr{ 85 | buckets: make([]*Bucket, G_config.BucketCount), 86 | jobChan: make([]chan*PushJob, G_config.BucketCount), 87 | dispatchChan: make(chan*PushJob, G_config.DispatchChannelSize), 88 | } 89 | for bucketIdx, _ = range connMgr.buckets { 90 | connMgr.buckets[bucketIdx] = InitBucket(bucketIdx) // 初始化Bucket 91 | connMgr.jobChan[bucketIdx] = make(chan*PushJob, G_config.BucketJobChannelSize) // Bucket的Job队列 92 | // Bucket的Job worker 93 | for jobWorkerIdx = 0; jobWorkerIdx < G_config.BucketJobWorkerCount; jobWorkerIdx++ { 94 | go connMgr.jobWorkerMain(jobWorkerIdx, bucketIdx) 95 | } 96 | } 97 | // 初始化分发协程, 用于将消息扇出给各个Bucket 98 | for dispatchWorkerIdx = 0; dispatchWorkerIdx < G_config.DispatchWorkerCount; dispatchWorkerIdx++ { 99 | go connMgr.dispatchWorkerMain(dispatchWorkerIdx) 100 | } 101 | 102 | G_connMgr = connMgr 103 | return 104 | } 105 | 106 | func (connMgr *ConnMgr) GetBucket(wsConnection *WSConnection) (bucket *Bucket) { 107 | bucket = connMgr.buckets[wsConnection.connId % uint64(len(connMgr.buckets))] 108 | return 109 | } 110 | 111 | func (connMgr *ConnMgr) AddConn(wsConnection *WSConnection) { 112 | var ( 113 | bucket *Bucket 114 | ) 115 | 116 | bucket = connMgr.GetBucket(wsConnection) 117 | bucket.AddConn(wsConnection) 118 | 119 | OnlineConnections_INCR() 120 | } 121 | 122 | func (connMgr *ConnMgr) DelConn(wsConnection *WSConnection) { 123 | var ( 124 | bucket *Bucket 125 | ) 126 | 127 | bucket = connMgr.GetBucket(wsConnection) 128 | bucket.DelConn(wsConnection) 129 | 130 | OnlineConnections_DESC() 131 | } 132 | 133 | func (connMgr *ConnMgr) JoinRoom(roomId string, wsConn *WSConnection) (err error) { 134 | var ( 135 | bucket *Bucket 136 | ) 137 | 138 | bucket = connMgr.GetBucket(wsConn) 139 | err = bucket.JoinRoom(roomId, wsConn) 140 | return 141 | } 142 | 143 | func (connMgr *ConnMgr) LeaveRoom(roomId string, wsConn *WSConnection) (err error) { 144 | var ( 145 | bucket *Bucket 146 | ) 147 | 148 | bucket = connMgr.GetBucket(wsConn) 149 | err = bucket.LeaveRoom(roomId, wsConn) 150 | return 151 | } 152 | 153 | // 向所有在线用户发送消息 154 | func (connMgr *ConnMgr) PushAll(bizMsg *common.BizMessage) (err error) { 155 | var ( 156 | pushJob *PushJob 157 | ) 158 | 159 | pushJob = &PushJob{ 160 | pushType: common.PUSH_TYPE_ALL, 161 | bizMsg: bizMsg, 162 | } 163 | 164 | select { 165 | case connMgr.dispatchChan <- pushJob: 166 | DispatchPending_INCR() 167 | default: 168 | err = common.ERR_DISPATCH_CHANNEL_FULL 169 | DispatchFail_INCR() 170 | } 171 | return 172 | } 173 | 174 | // 向指定房间发送消息 175 | func (connMgr *ConnMgr) PushRoom(roomId string, bizMsg *common.BizMessage) (err error) { 176 | var ( 177 | pushJob *PushJob 178 | ) 179 | 180 | pushJob = &PushJob{ 181 | pushType: common.PUSH_TYPE_ROOM, 182 | bizMsg: bizMsg, 183 | roomId: roomId, 184 | } 185 | 186 | select { 187 | case connMgr.dispatchChan <- pushJob: 188 | DispatchPending_INCR() 189 | default: 190 | err = common.ERR_DISPATCH_CHANNEL_FULL 191 | DispatchFail_INCR() 192 | } 193 | return 194 | } -------------------------------------------------------------------------------- /gateway/Merger.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | "github.com/owenliang/go-push/common" 7 | ) 8 | 9 | type PushBatch struct { 10 | items []*json.RawMessage 11 | commitTimer *time.Timer 12 | 13 | // union { 14 | room string // 按room合并 15 | // } 16 | } 17 | 18 | type PushContext struct { 19 | msg *json.RawMessage 20 | 21 | // union { 22 | room string // 按room合并 23 | // } 24 | } 25 | 26 | type MergeWorker struct { 27 | mergeType int // 合并类型: 广播, room, uid... 28 | 29 | contextChan chan*PushContext 30 | timeoutChan chan*PushBatch 31 | 32 | // union { 33 | room2Batch map[string]*PushBatch // room合并 34 | allBatch *PushBatch // 广播合并 35 | // } 36 | } 37 | 38 | // 广播消息、房间消息的合并 39 | type Merger struct { 40 | roomWorkers []*MergeWorker // 房间合并 41 | broadcastWorker *MergeWorker // 广播合并 42 | } 43 | 44 | var ( 45 | G_merger *Merger 46 | ) 47 | 48 | func (worker *MergeWorker) autoCommit(batch *PushBatch) func() { 49 | return func() { 50 | worker.timeoutChan <- batch 51 | } 52 | } 53 | 54 | func (worker *MergeWorker) commitBatch(batch *PushBatch) (err error) { 55 | var ( 56 | bizPushData *common.BizPushData 57 | bizMessage *common.BizMessage 58 | buf []byte 59 | ) 60 | 61 | bizPushData = &common.BizPushData{ 62 | Items: batch.items, 63 | } 64 | if buf, err = json.Marshal(*bizPushData); err != nil { 65 | return 66 | } 67 | 68 | bizMessage = &common.BizMessage{ 69 | Type: "PUSH", 70 | Data: json.RawMessage(buf), 71 | } 72 | 73 | // 打包发送 74 | if worker.mergeType == common.PUSH_TYPE_ROOM { 75 | delete(worker.room2Batch, batch.room) 76 | err = G_connMgr.PushRoom(batch.room, bizMessage) 77 | } else if worker.mergeType == common.PUSH_TYPE_ALL { 78 | worker.allBatch = nil 79 | err = G_connMgr.PushAll(bizMessage) 80 | } 81 | return 82 | } 83 | 84 | func (worker *MergeWorker) mergeWorkerMain() { 85 | var ( 86 | context *PushContext 87 | batch *PushBatch 88 | timeoutBatch *PushBatch 89 | existed bool 90 | isCreated bool 91 | err error 92 | ) 93 | for { 94 | select { 95 | case context = <- worker.contextChan: 96 | MergerPending_DESC() 97 | 98 | isCreated = false 99 | // 按房间合并 100 | if worker.mergeType == common.PUSH_TYPE_ROOM { 101 | if batch, existed = worker.room2Batch[context.room]; !existed { 102 | batch = &PushBatch{room: context.room} 103 | worker.room2Batch[context.room] = batch 104 | isCreated = true 105 | } 106 | } else if worker.mergeType == common.PUSH_TYPE_ALL { // 广播合并 107 | batch = worker.allBatch 108 | if batch == nil { 109 | batch = &PushBatch{} 110 | worker.allBatch = batch 111 | isCreated = true 112 | } 113 | } 114 | 115 | // 合并消息 116 | batch.items = append(batch.items, context.msg) 117 | 118 | // 新建批次, 启动超时自动提交 119 | if isCreated { 120 | batch.commitTimer = time.AfterFunc(time.Duration(G_config.MaxMergerDelay) * time.Millisecond, worker.autoCommit(batch)) 121 | } 122 | 123 | // 批次未满, 继续等待下次提交 124 | if len(batch.items) < G_config.MaxMergerBatchSize { 125 | continue 126 | } 127 | 128 | // 批次已满, 取消超时自动提交 129 | batch.commitTimer.Stop() 130 | case timeoutBatch = <- worker.timeoutChan: 131 | if worker.mergeType == common.PUSH_TYPE_ROOM { 132 | // 定时器触发时, 批次已被提交 133 | if batch, existed = worker.room2Batch[timeoutBatch.room]; !existed { 134 | continue 135 | } 136 | 137 | // 定时器触发时, 前一个批次已提交, 下一个批次已建立 138 | if batch != timeoutBatch { 139 | continue 140 | } 141 | } else if worker.mergeType == common.PUSH_TYPE_ALL { 142 | batch = worker.allBatch 143 | // 定时器触发时, 批次已被提交 144 | if timeoutBatch != batch { 145 | continue 146 | } 147 | } 148 | } 149 | // 提交批次 150 | err = worker.commitBatch(batch) 151 | 152 | // 打点统计 153 | if worker.mergeType == common.PUSH_TYPE_ALL { 154 | MergerAllTotal_INCR(int64(len(batch.items))) 155 | if err != nil { 156 | MergerAllFail_INCR(int64(len(batch.items))) 157 | } 158 | } else if worker.mergeType == common.PUSH_TYPE_ROOM { 159 | MergerRoomTotal_INCR(int64(len(batch.items))) 160 | if err != nil { 161 | MergerRoomFail_INCR(int64(len(batch.items))) 162 | } 163 | } 164 | } 165 | } 166 | 167 | func initMergeWorker(mergeType int) (worker *MergeWorker) { 168 | worker = &MergeWorker{ 169 | mergeType: mergeType, 170 | room2Batch: make(map[string]*PushBatch), 171 | contextChan: make(chan*PushContext, G_config.MergerChannelSize), 172 | timeoutChan: make(chan*PushBatch, G_config.MergerChannelSize), 173 | } 174 | go worker.mergeWorkerMain() 175 | return 176 | } 177 | 178 | func (worker *MergeWorker) pushRoom(room string, msg *json.RawMessage) (err error) { 179 | var ( 180 | context *PushContext 181 | ) 182 | context = &PushContext{ 183 | room: room, 184 | msg: msg, 185 | } 186 | select { 187 | case worker.contextChan <- context: 188 | MergerPending_INCR() 189 | default: 190 | err = common.ERR_MERGE_CHANNEL_FULL 191 | } 192 | return 193 | } 194 | 195 | func (worker *MergeWorker) pushAll(msg *json.RawMessage) (err error) { 196 | var ( 197 | context *PushContext 198 | ) 199 | context = &PushContext{ 200 | msg: msg, 201 | } 202 | select { 203 | case worker.contextChan <- context: 204 | MergerPending_INCR() 205 | default: 206 | err = common.ERR_MERGE_CHANNEL_FULL 207 | } 208 | return 209 | } 210 | 211 | /** 212 | API 213 | */ 214 | 215 | func InitMerger() (err error) { 216 | var ( 217 | workerIdx int 218 | merger *Merger 219 | ) 220 | 221 | merger = &Merger{ 222 | roomWorkers: make([]*MergeWorker, G_config.MergerWorkerCount), 223 | } 224 | for workerIdx = 0; workerIdx < G_config.MergerWorkerCount; workerIdx++ { 225 | merger.roomWorkers[workerIdx] = initMergeWorker(common.PUSH_TYPE_ROOM) 226 | } 227 | merger.broadcastWorker = initMergeWorker(common.PUSH_TYPE_ALL) 228 | 229 | G_merger = merger 230 | return 231 | } 232 | 233 | // 广播合并推送 234 | func (merger *Merger) PushAll(msg *json.RawMessage) (err error) { 235 | return merger.broadcastWorker.pushAll(msg) 236 | } 237 | 238 | // 房间合并推送 239 | func (merger *Merger) PushRoom(room string, msg *json.RawMessage) (err error) { 240 | // 计算room hash到某个worker 241 | var ( 242 | workerIdx uint32= 0 243 | ch byte 244 | ) 245 | for _, ch = range []byte(room) { 246 | workerIdx = (workerIdx + uint32(ch) * 33) % uint32(G_config.MergerWorkerCount) 247 | } 248 | return merger.roomWorkers[workerIdx].pushRoom(room, msg) 249 | } -------------------------------------------------------------------------------- /gateway/Room.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "sync" 5 | "github.com/owenliang/go-push/common" 6 | ) 7 | 8 | // 房间 9 | type Room struct { 10 | rwMutex sync.RWMutex 11 | roomId string 12 | id2Conn map[uint64]*WSConnection 13 | } 14 | 15 | func InitRoom(roomId string) (room *Room) { 16 | room = &Room { 17 | roomId: roomId, 18 | id2Conn: make(map[uint64]*WSConnection), 19 | } 20 | return 21 | } 22 | 23 | func (room *Room) Join(wsConn *WSConnection) (err error) { 24 | var ( 25 | existed bool 26 | ) 27 | 28 | room.rwMutex.Lock() 29 | defer room.rwMutex.Unlock() 30 | 31 | if _, existed = room.id2Conn[wsConn.connId]; existed { 32 | err = common.ERR_JOIN_ROOM_TWICE 33 | return 34 | } 35 | 36 | room.id2Conn[wsConn.connId] = wsConn 37 | return 38 | } 39 | 40 | func (room *Room) Leave(wsConn* WSConnection) (err error) { 41 | var ( 42 | existed bool 43 | ) 44 | 45 | room.rwMutex.Lock() 46 | defer room.rwMutex.Unlock() 47 | 48 | if _, existed = room.id2Conn[wsConn.connId]; !existed { 49 | err = common.ERR_NOT_IN_ROOM 50 | return 51 | } 52 | 53 | delete(room.id2Conn, wsConn.connId) 54 | return 55 | } 56 | 57 | func (room *Room) Count() int { 58 | room.rwMutex.RLock() 59 | defer room.rwMutex.RUnlock() 60 | 61 | return len(room.id2Conn) 62 | } 63 | 64 | func (room *Room) Push(wsMsg *common.WSMessage) { 65 | var ( 66 | wsConn *WSConnection 67 | ) 68 | room.rwMutex.RLock() 69 | defer room.rwMutex.RUnlock() 70 | 71 | for _, wsConn = range room.id2Conn { 72 | wsConn.SendMessage(wsMsg) 73 | } 74 | } -------------------------------------------------------------------------------- /gateway/Service.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | "net" 7 | "strconv" 8 | "encoding/json" 9 | "crypto/tls" 10 | "github.com/owenliang/go-push/common" 11 | ) 12 | 13 | type Service struct { 14 | server *http.Server 15 | } 16 | 17 | var ( 18 | G_service *Service 19 | ) 20 | 21 | // 全量推送POST msg={} 22 | func handlePushAll(resp http.ResponseWriter, req *http.Request) { 23 | var ( 24 | err error 25 | items string 26 | msgArr []json.RawMessage 27 | msgIdx int 28 | ) 29 | if err = req.ParseForm(); err != nil { 30 | return 31 | } 32 | 33 | items = req.PostForm.Get("items") 34 | if err = json.Unmarshal([]byte(items), &msgArr); err != nil { 35 | return 36 | } 37 | 38 | for msgIdx, _ = range msgArr { 39 | G_merger.PushAll(&msgArr[msgIdx]) 40 | } 41 | } 42 | 43 | // 房间推送POST room=xxx&msg 44 | func handlePushRoom(resp http.ResponseWriter, req *http.Request) { 45 | var ( 46 | err error 47 | room string 48 | items string 49 | msgArr []json.RawMessage 50 | msgIdx int 51 | ) 52 | if err = req.ParseForm(); err != nil { 53 | return 54 | } 55 | 56 | room = req.PostForm.Get("room") 57 | items = req.PostForm.Get("items") 58 | 59 | if err = json.Unmarshal([]byte(items), &msgArr); err != nil { 60 | return 61 | } 62 | 63 | for msgIdx, _ = range msgArr { 64 | G_merger.PushRoom(room, &msgArr[msgIdx]) 65 | } 66 | } 67 | 68 | // 统计 69 | func handleStats(resp http.ResponseWriter, req *http.Request) { 70 | var ( 71 | data []byte 72 | err error 73 | ) 74 | 75 | if data, err = G_stats.Dump(); err != nil { 76 | return 77 | } 78 | 79 | resp.Write(data) 80 | } 81 | 82 | func InitService() (err error) { 83 | var ( 84 | mux *http.ServeMux 85 | server *http.Server 86 | listener net.Listener 87 | ) 88 | 89 | // 路由 90 | mux = http.NewServeMux() 91 | mux.HandleFunc("/push/all", handlePushAll) 92 | mux.HandleFunc("/push/room", handlePushRoom) 93 | mux.HandleFunc("/stats", handleStats) 94 | 95 | // TLS证书解析验证 96 | if _, err = tls.LoadX509KeyPair(G_config.ServerPem, G_config.ServerKey); err != nil { 97 | return common.ERR_CERT_INVALID 98 | } 99 | 100 | // HTTP/2 TLS服务 101 | server = &http.Server{ 102 | ReadTimeout: time.Duration(G_config.ServiceReadTimeout) * time.Millisecond, 103 | WriteTimeout: time.Duration(G_config.ServiceWriteTimeout) * time.Millisecond, 104 | Handler: mux, 105 | } 106 | 107 | // 监听端口 108 | if listener, err = net.Listen("tcp", ":" + strconv.Itoa(G_config.ServicePort)); err != nil { 109 | return 110 | } 111 | 112 | // 赋值全局变量 113 | G_service = &Service{ 114 | server: server, 115 | } 116 | 117 | // 拉起服务 118 | go server.ServeTLS(listener, G_config.ServerPem, G_config.ServerKey) 119 | 120 | return 121 | } -------------------------------------------------------------------------------- /gateway/Stats.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "sync/atomic" 5 | "encoding/json" 6 | ) 7 | 8 | type Stats struct { 9 | // 反馈在线长连接的数量 10 | OnlineConnections int64 `json:"onlineConnections"` 11 | 12 | // 反馈客户端的推送压力 13 | SendMessageTotal int64 `json:"sendMessageTotal"` 14 | SendMessageFail int64 `json:"sendMessageFail"` 15 | 16 | // 反馈ConnMgr消息分发模块的压力 17 | DispatchPending int64 `json:"dispatchPending"` 18 | PushJobPending int64 `json:"pushJobPending"` 19 | DispatchFail int64 `json:"dispatchFail"` 20 | 21 | // 返回出在线的房间总数, 有利于分析内存上涨的原因 22 | RoomCount int64 `json:"roomCount"` 23 | 24 | // Merger模块处理队列, 反馈出消息合并的压力情况 25 | MergerPending int64 `json:"mergerPending"` 26 | 27 | // Merger模块合并发送的消息总数与失败总数 28 | MergerRoomTotal int64 `json:"mergerRoomTotal"` 29 | MergerAllTotal int64 `json:"mergerAllTotal"` 30 | MergerRoomFail int64 `json:"mergerRoomFail"` 31 | MergerAllFail int64 `json:"mergerAllFail"` 32 | } 33 | 34 | var ( 35 | G_stats *Stats 36 | ) 37 | 38 | func InitStats() (err error) { 39 | G_stats = &Stats{} 40 | return 41 | } 42 | 43 | func DispatchPending_INCR() { 44 | atomic.AddInt64(&G_stats.DispatchPending, 1) 45 | } 46 | 47 | func DispatchPending_DESC() { 48 | atomic.AddInt64(&G_stats.DispatchPending, -1) 49 | } 50 | 51 | func PushJobPending_INCR() { 52 | atomic.AddInt64(&G_stats.PushJobPending, 1) 53 | } 54 | 55 | func PushJobPending_DESC() { 56 | atomic.AddInt64(&G_stats.PushJobPending, -1) 57 | } 58 | 59 | func OnlineConnections_INCR() { 60 | atomic.AddInt64(&G_stats.OnlineConnections, 1) 61 | } 62 | 63 | func OnlineConnections_DESC() { 64 | atomic.AddInt64(&G_stats.OnlineConnections, -1) 65 | } 66 | 67 | func RoomCount_INCR() { 68 | atomic.AddInt64(&G_stats.RoomCount, 1) 69 | } 70 | 71 | func RoomCount_DESC() { 72 | atomic.AddInt64(&G_stats.RoomCount, -1) 73 | } 74 | 75 | func MergerPending_INCR() { 76 | atomic.AddInt64(&G_stats.MergerPending, 1) 77 | } 78 | 79 | func MergerPending_DESC() { 80 | atomic.AddInt64(&G_stats.MergerPending, -1) 81 | } 82 | 83 | func MergerRoomTotal_INCR(batchSize int64) { 84 | atomic.AddInt64(&G_stats.MergerRoomTotal, batchSize) 85 | } 86 | 87 | func MergerAllTotal_INCR(batchSize int64) { 88 | atomic.AddInt64(&G_stats.MergerAllTotal, batchSize) 89 | } 90 | 91 | func MergerRoomFail_INCR(batchSize int64) { 92 | atomic.AddInt64(&G_stats.MergerRoomFail, batchSize) 93 | } 94 | 95 | func MergerAllFail_INCR(batchSize int64) { 96 | atomic.AddInt64(&G_stats.MergerAllFail, batchSize) 97 | } 98 | 99 | func DispatchFail_INCR() { 100 | atomic.AddInt64(&G_stats.DispatchFail, 1) 101 | } 102 | 103 | func SendMessageFail_INCR() { 104 | atomic.AddInt64(&G_stats.SendMessageFail, 1) 105 | } 106 | 107 | func SendMessageTotal_INCR() { 108 | atomic.AddInt64(&G_stats.SendMessageTotal, 1) 109 | } 110 | 111 | func (stats *Stats) Dump() (data []byte, err error){ 112 | return json.Marshal(G_stats) 113 | } -------------------------------------------------------------------------------- /gateway/WSConnection.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "sync" 6 | "time" 7 | "github.com/owenliang/go-push/common" 8 | ) 9 | 10 | type WSConnection struct { 11 | mutex sync.Mutex 12 | connId uint64 13 | wsSocket *websocket.Conn 14 | inChan chan*common.WSMessage 15 | outChan chan*common.WSMessage 16 | closeChan chan byte 17 | isClosed bool 18 | lastHeartbeatTime time.Time // 最近一次心跳时间 19 | rooms map[string]bool // 加入了哪些房间 20 | } 21 | 22 | // 读websocket 23 | func (wsConnection *WSConnection) readLoop() { 24 | var ( 25 | msgType int 26 | msgData []byte 27 | message *common.WSMessage 28 | err error 29 | ) 30 | for { 31 | if msgType, msgData, err = wsConnection.wsSocket.ReadMessage(); err != nil { 32 | goto ERR 33 | } 34 | 35 | message = common.BuildWSMessage(msgType, msgData) 36 | 37 | select { 38 | case wsConnection.inChan <- message: 39 | case <- wsConnection.closeChan: 40 | goto CLOSED 41 | } 42 | } 43 | 44 | ERR: 45 | wsConnection.Close() 46 | CLOSED: 47 | } 48 | 49 | // 写websocket 50 | func (wsConnection *WSConnection) writeLoop() { 51 | var ( 52 | message *common.WSMessage 53 | err error 54 | ) 55 | for { 56 | select { 57 | case message = <- wsConnection.outChan: 58 | if err = wsConnection.wsSocket.WriteMessage(message.MsgType, message.MsgData); err != nil { 59 | goto ERR 60 | } 61 | case <- wsConnection.closeChan: 62 | goto CLOSED 63 | } 64 | } 65 | ERR: 66 | wsConnection.Close() 67 | CLOSED: 68 | } 69 | 70 | /** 71 | 以下是API 72 | */ 73 | 74 | func InitWSConnection(connId uint64, wsSocket *websocket.Conn) (wsConnection *WSConnection) { 75 | wsConnection = &WSConnection{ 76 | wsSocket: wsSocket, 77 | connId: connId, 78 | inChan: make(chan *common.WSMessage, G_config.WsInChannelSize), 79 | outChan: make(chan *common.WSMessage, G_config.WsOutChannelSize), 80 | closeChan: make(chan byte), 81 | lastHeartbeatTime: time.Now(), 82 | rooms: make(map[string]bool), 83 | } 84 | 85 | go wsConnection.readLoop() 86 | go wsConnection.writeLoop() 87 | 88 | return 89 | } 90 | 91 | // 发送消息 92 | func (wsConnection *WSConnection) SendMessage(message *common.WSMessage) (err error) { 93 | select { 94 | case wsConnection.outChan <- message: 95 | SendMessageTotal_INCR() 96 | case <- wsConnection.closeChan: 97 | err = common.ERR_CONNECTION_LOSS 98 | default: // 写操作不会阻塞, 因为channel已经预留给websocket一定的缓冲空间 99 | err = common.ERR_SEND_MESSAGE_FULL 100 | SendMessageFail_INCR() 101 | } 102 | return 103 | } 104 | 105 | // 读取消息 106 | func (wsConnection *WSConnection) ReadMessage() (message *common.WSMessage, err error) { 107 | select { 108 | case message = <- wsConnection.inChan: 109 | case <- wsConnection.closeChan: 110 | err = common.ERR_CONNECTION_LOSS 111 | } 112 | return 113 | } 114 | 115 | // 关闭连接 116 | func (wsConnection *WSConnection) Close() { 117 | wsConnection.wsSocket.Close() 118 | 119 | wsConnection.mutex.Lock() 120 | defer wsConnection.mutex.Unlock() 121 | 122 | if !wsConnection.isClosed { 123 | wsConnection.isClosed = true 124 | close(wsConnection.closeChan) 125 | } 126 | } 127 | 128 | // 检查心跳(不需要太频繁) 129 | func (wsConnection *WSConnection) IsAlive() bool { 130 | var ( 131 | now = time.Now() 132 | ) 133 | 134 | wsConnection.mutex.Lock() 135 | defer wsConnection.mutex.Unlock() 136 | 137 | // 连接已关闭 或者 太久没有心跳 138 | if wsConnection.isClosed || now.Sub(wsConnection.lastHeartbeatTime) > time.Duration(G_config.WsHeartbeatInterval) * time.Second { 139 | return false 140 | } 141 | return true 142 | } 143 | 144 | // 更新心跳 145 | func (WSConnection *WSConnection) KeepAlive() { 146 | var ( 147 | now = time.Now() 148 | ) 149 | 150 | WSConnection.mutex.Lock() 151 | defer WSConnection.mutex.Unlock() 152 | 153 | WSConnection.lastHeartbeatTime = now 154 | } -------------------------------------------------------------------------------- /gateway/WSHandler.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "time" 5 | "github.com/gorilla/websocket" 6 | "encoding/json" 7 | "github.com/owenliang/go-push/common" 8 | ) 9 | 10 | // 每隔1秒, 检查一次连接是否健康 11 | func (wsConnection *WSConnection) heartbeatChecker() { 12 | var ( 13 | timer *time.Timer 14 | ) 15 | timer = time.NewTimer(time.Duration(G_config.WsHeartbeatInterval) * time.Second) 16 | for { 17 | select { 18 | case <- timer.C: 19 | if !wsConnection.IsAlive() { 20 | wsConnection.Close() 21 | goto EXIT 22 | } 23 | timer.Reset(time.Duration(G_config.WsHeartbeatInterval) * time.Second) 24 | case <- wsConnection.closeChan: 25 | timer.Stop() 26 | goto EXIT 27 | } 28 | } 29 | 30 | EXIT: 31 | // 确保连接被关闭 32 | } 33 | 34 | // 处理PING请求 35 | func (wsConnection *WSConnection) handlePing(bizReq *common.BizMessage) (bizResp *common.BizMessage, err error) { 36 | var ( 37 | buf []byte 38 | ) 39 | 40 | wsConnection.KeepAlive() 41 | 42 | if buf, err = json.Marshal(common.BizPongData{}); err != nil { 43 | return 44 | } 45 | bizResp = &common.BizMessage{ 46 | Type: "PONG", 47 | Data: json.RawMessage(buf), 48 | } 49 | return 50 | } 51 | 52 | // 处理JOIN请求 53 | func (wsConnection *WSConnection) handleJoin(bizReq *common.BizMessage) (bizResp *common.BizMessage, err error) { 54 | var ( 55 | bizJoinData *common.BizJoinData 56 | existed bool 57 | ) 58 | bizJoinData = &common.BizJoinData{} 59 | if err = json.Unmarshal(bizReq.Data, bizJoinData); err != nil { 60 | return 61 | } 62 | if len(bizJoinData.Room) == 0 { 63 | err = common.ERR_ROOM_ID_INVALID 64 | return 65 | } 66 | if len(wsConnection.rooms) >= G_config.MaxJoinRoom { 67 | // 超过了房间数量限制, 忽略这个请求 68 | return 69 | } 70 | // 已加入过 71 | if _, existed = wsConnection.rooms[bizJoinData.Room]; existed { 72 | // 忽略掉这个请求 73 | return 74 | } 75 | // 建立房间 -> 连接的关系 76 | if err = G_connMgr.JoinRoom(bizJoinData.Room, wsConnection); err != nil { 77 | return 78 | } 79 | // 建立连接 -> 房间的关系 80 | wsConnection.rooms[bizJoinData.Room] = true 81 | return 82 | } 83 | 84 | // 处理LEAVE请求 85 | func (wsConnection *WSConnection) handleLeave(bizReq *common.BizMessage) (bizResp *common.BizMessage, err error) { 86 | var ( 87 | bizLeaveData *common.BizLeaveData 88 | existed bool 89 | ) 90 | bizLeaveData = &common.BizLeaveData{} 91 | if err = json.Unmarshal(bizReq.Data, bizLeaveData); err != nil { 92 | return 93 | } 94 | if len(bizLeaveData.Room) == 0 { 95 | err = common.ERR_ROOM_ID_INVALID 96 | return 97 | } 98 | // 未加入过 99 | if _, existed = wsConnection.rooms[bizLeaveData.Room]; !existed { 100 | // 忽略掉这个请求 101 | return 102 | } 103 | // 删除房间 -> 连接的关系 104 | if err = G_connMgr.LeaveRoom(bizLeaveData.Room, wsConnection); err != nil { 105 | return 106 | } 107 | // 删除连接 -> 房间的关系 108 | delete(wsConnection.rooms, bizLeaveData.Room) 109 | return 110 | } 111 | 112 | func (wsConnection *WSConnection) leaveAll() { 113 | var ( 114 | roomId string 115 | ) 116 | // 从所有房间中退出 117 | for roomId, _ = range wsConnection.rooms { 118 | G_connMgr.LeaveRoom(roomId, wsConnection) 119 | delete(wsConnection.rooms, roomId) 120 | } 121 | } 122 | 123 | // 处理websocket请求 124 | func (wsConnection *WSConnection) WSHandle() { 125 | var ( 126 | message *common.WSMessage 127 | bizReq *common.BizMessage 128 | bizResp *common.BizMessage 129 | err error 130 | buf []byte 131 | ) 132 | 133 | // 连接加入管理器, 可以推送端查找到 134 | G_connMgr.AddConn(wsConnection) 135 | 136 | // 心跳检测线程 137 | go wsConnection.heartbeatChecker() 138 | 139 | // 请求处理协程 140 | for { 141 | if message, err = wsConnection.ReadMessage(); err != nil { 142 | goto ERR 143 | } 144 | 145 | // 只处理文本消息 146 | if message.MsgType != websocket.TextMessage { 147 | continue 148 | } 149 | 150 | // 解析消息体 151 | if bizReq, err = common.DecodeBizMessage(message.MsgData); err != nil { 152 | goto ERR 153 | } 154 | 155 | bizResp = nil 156 | 157 | // 1,收到PING则响应PONG: {"type": "PING"}, {"type": "PONG"} 158 | // 2,收到JOIN则加入ROOM: {"type": "JOIN", "data": {"room": "chrome-plugin"}} 159 | // 3,收到LEAVE则离开ROOM: {"type": "LEAVE", "data": {"room": "chrome-plugin"}} 160 | 161 | // 请求串行处理 162 | switch bizReq.Type { 163 | case "PING": 164 | if bizResp, err = wsConnection.handlePing(bizReq); err != nil { 165 | goto ERR 166 | } 167 | case "JOIN": 168 | if bizResp, err = wsConnection.handleJoin(bizReq); err != nil { 169 | goto ERR 170 | } 171 | case "LEAVE": 172 | if bizResp, err = wsConnection.handleLeave(bizReq); err != nil { 173 | goto ERR 174 | } 175 | } 176 | 177 | if bizResp != nil { 178 | if buf, err = json.Marshal(*bizResp); err != nil { 179 | goto ERR 180 | } 181 | // socket缓冲区写满不是致命错误 182 | if err = wsConnection.SendMessage(&common.WSMessage{websocket.TextMessage, buf}); err != nil { 183 | if err != common.ERR_SEND_MESSAGE_FULL { 184 | goto ERR 185 | } else { 186 | err = nil 187 | } 188 | } 189 | } 190 | } 191 | 192 | ERR: 193 | // 确保连接关闭 194 | wsConnection.Close() 195 | 196 | // 离开所有房间 197 | wsConnection.leaveAll() 198 | 199 | // 从连接池中移除 200 | G_connMgr.DelConn(wsConnection) 201 | return 202 | } -------------------------------------------------------------------------------- /gateway/WSServer.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | "net" 7 | "strconv" 8 | "github.com/gorilla/websocket" 9 | "sync/atomic" 10 | ) 11 | 12 | // WebSocket服务端 13 | type WSServer struct { 14 | server *http.Server 15 | curConnId uint64 16 | } 17 | 18 | var ( 19 | G_wsServer *WSServer 20 | 21 | wsUpgrader = websocket.Upgrader{ 22 | // 允许所有CORS跨域请求 23 | CheckOrigin: func(r *http.Request) bool { 24 | return true 25 | }, 26 | } 27 | ) 28 | 29 | func handleConnect(resp http.ResponseWriter, req *http.Request) { 30 | var ( 31 | err error 32 | wsSocket *websocket.Conn 33 | connId uint64 34 | wsConn *WSConnection 35 | ) 36 | 37 | // WebSocket握手 38 | if wsSocket, err = wsUpgrader.Upgrade(resp, req, nil); err != nil { 39 | return 40 | } 41 | 42 | // 连接唯一标识 43 | connId = atomic.AddUint64(&G_wsServer.curConnId, 1) 44 | 45 | // 初始化WebSocket的读写协程 46 | wsConn = InitWSConnection(connId, wsSocket) 47 | 48 | // 开始处理websocket消息 49 | wsConn.WSHandle() 50 | } 51 | 52 | func InitWSServer() (err error) { 53 | var ( 54 | mux *http.ServeMux 55 | server *http.Server 56 | listener net.Listener 57 | ) 58 | 59 | // 路由 60 | mux = http.NewServeMux() 61 | mux.HandleFunc("/connect", handleConnect) 62 | 63 | // HTTP服务 64 | server = &http.Server{ 65 | ReadTimeout: time.Duration(G_config.WsReadTimeout) * time.Millisecond, 66 | WriteTimeout: time.Duration(G_config.WsWriteTimeout) * time.Millisecond, 67 | Handler: mux, 68 | } 69 | 70 | // 监听端口 71 | if listener, err = net.Listen("tcp", ":" + strconv.Itoa(G_config.WsPort)); err != nil { 72 | return 73 | } 74 | 75 | // 赋值全局变量 76 | G_wsServer = &WSServer{ 77 | server: server, 78 | curConnId: uint64(time.Now().Unix()), 79 | } 80 | 81 | // 拉起服务 82 | go server.Serve(listener) 83 | 84 | return 85 | } -------------------------------------------------------------------------------- /gateway/cli/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 63 | 64 | 65 |
67 | Click "Open" to create a connection to the server, 68 | "Send" to send a message to the server and "Close" to close the connection. 69 | You can change the message and send multiple times. 70 | 71 | 78 | | 79 | 80 | |