├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── common
├── error_code.go
└── rsp_common.go
├── config
└── app.yaml.example
├── controllers
├── base_controller.go
├── home
│ └── home_controller.go
├── systems
│ └── system_controller.go
└── user
│ └── user_controller.go
├── go.mod
├── go.sum
├── helper
├── order_helper.go
└── server_helper.go
├── img
├── HTTP协议和WebSocket比较.jpeg
├── HTTP协议和WebSocket比较.png
├── jetbrains_logo.png
├── websocket接收和发送数据.png
├── 分布是系统随机给用户发送消息.png
├── 奥运五环.png
├── 微信二维码.jpeg
├── 服务端处理一个请求.jpeg
├── 浏览器 Network.png
├── 浏览器开始支持webSocket的版本.jpeg
├── 用户连接时序图.png
└── 网站架构图.png
├── lib
├── cache
│ ├── server_cache.go
│ ├── submit_cache.go
│ └── user_cache.go
└── redislib
│ └── redis_lib.go
├── log
└── .gitignore
├── main.go
├── models
├── msg_model.go
├── request_model.go
├── response_model.go
├── server_model.go
└── user_model.go
├── protobuf
├── gen.sh
├── im_protobuf.pb.go
├── im_protobuf.proto
└── im_protobuf_grpc.pb.go
├── routers
├── acc_routers.go
└── web_routers.go
├── servers
├── grpcclient
│ └── grpc_client.go
├── grpcserver
│ └── grpc_server.go
├── task
│ ├── clean_connection _task.go
│ ├── server_task.go
│ └── task_init.go
└── websocket
│ ├── acc_controller.go
│ ├── acc_process.go
│ ├── client.go
│ ├── client_manager.go
│ ├── init_acc.go
│ └── user_srv.go
├── views
└── home
│ ├── index.html
│ └── index.tpl
└── 目录.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | .idea/
15 | *.swp
16 | *.log
17 | .DS_Store
18 | config/app.yaml
19 | main
20 | main_linux
21 | gowebsocket
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 link1st
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: run
2 | run:
3 | go run main.go
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于websocket单台机器支持百万连接分布式聊天(IM)系统
2 | [](https://pkg.go.dev/github.com/link1st/gowebsocket/v2)
3 | [](https://github.com/link1st/gowebsocket/releases)
4 | [](https://goreportcard.com/report/github.com/link1st/gowebsocket/v2)
5 | [](https://github.com/link1st/gowebsocket/issues)
6 | [](https://github.com/link1st/gowebsocket/issues?q=is%3Aissue+is%3Aclosed)
7 | 
8 | 
9 | [](https://starchart.cc/link1st/gowebsocket)
10 |
11 | 本文将介绍如何实现一个基于websocket分布式聊天(IM)系统。
12 |
13 | 使用golang实现websocket通讯,单机可以支持百万连接,使用gin框架、nginx负载、可以水平部署、程序内部相互通讯、使用grpc通讯协议。
14 |
15 | 本文内容比较长,如果直接想clone项目体验直接进入[项目体验](#12-项目体验) [goWebSocket项目下载](#4goWebSocket-项目) ,文本从介绍webSocket是什么开始,然后开始介绍这个项目,以及在Nginx中配置域名做webSocket的转发,然后介绍如何搭建一个分布式系统。
16 |
17 |
18 | ## 目录
19 | - [1、项目说明](#1项目说明)
20 | - [1.1 goWebSocket](#11-goWebSocket)
21 | - [1.2 项目体验](#12-项目体验)
22 | - [2、介绍webSocket](#2介绍webSocket)
23 | - [2.1 webSocket 是什么](#21-webSocket-是什么)
24 | - [2.2 webSocket的兼容性](#22-webSocket的兼容性)
25 | - [2.3 为什么要用webSocket](#23-为什么要用webSocket)
26 | - [2.4 webSocket建立过程](#24-webSocket建立过程)
27 | - [3、如何实现基于webSocket的长连接系统](#3如何实现基于webSocket的长连接系统)
28 | - [3.1 使用go实现webSocket服务端](#31-使用go实现webSocket服务端)
29 | - [3.1.1 启动端口监听](#311-启动端口监听)
30 | - [3.1.2 升级协议](#312-升级协议)
31 | - [3.1.3 客户端连接的管理](#313-客户端连接的管理)
32 | - [3.1.4 注册客户端的socket的写的异步处理程序](#314-注册客户端的socket的写的异步处理程序)
33 | - [3.1.5 注册客户端的socket的读的异步处理程序](#315-注册客户端的socket的读的异步处理程序)
34 | - [3.1.6 接收客户端数据并处理](#316-接收客户端数据并处理)
35 | - [3.1.7 使用路由的方式处理客户端的请求数据](#317-使用路由的方式处理客户端的请求数据)
36 | - [3.1.8 防止内存溢出和Goroutine不回收](#318-防止内存溢出和Goroutine不回收)
37 | - [3.2 使用javaScript实现webSocket客户端](#32-使用javaScript实现webSocket客户端)
38 | - [3.2.1 启动并注册监听程序](#321-启动并注册监听程序)
39 | - [3.2.2 发送数据](#322-发送数据)
40 | - [3.3 发送消息](#33-发送消息)
41 | - [3.3.1 文本消息](#331-文本消息)
42 | - [3.3.2 图片和语言消息](#332-图片和语言消息)
43 | - [4、goWebSocket 项目](#4goWebSocket-项目)
44 | - [4.1 项目说明](#41-项目说明)
45 | - [4.2 项目依赖](#42-项目依赖)
46 | - [4.3 项目启动](#43-项目启动)
47 | - [4.4 接口文档](#44-接口文档)
48 | - [4.4.1 HTTP接口文档](#441-HTTP接口文档)
49 | - [4.4.1.1 接口说明](#4411-接口说明)
50 | - [4.4.1.2 聊天页面](#4412-聊天页面)
51 | - [4.4.1.3 获取房间用户列表](#4413-获取房间用户列表)
52 | - [4.4.1.4 查询用户是否在线](#4414-查询用户是否在线)
53 | - [4.4.1.5 给用户发送消息](#4415-给用户发送消息)
54 | - [4.4.1.6 给全员用户发送消息](#4416-给全员用户发送消息)
55 | - [4.4.2 RPC接口文档](#442-RPC接口文档)
56 | - [4.4.2.1 接口说明](#4421-接口说明)
57 | - [4.4.2.2 查询用户是否在线](#4422-查询用户是否在线)
58 | - [4.4.2.3 发送消息](#4423-发送消息)
59 | - [4.4.2.4 给指定房间所有用户发送消息](#4424-给指定房间所有用户发送消息)
60 | - [4.4.2.5 获取房间内全部用户](#4425-获取房间内全部用户)
61 | - [5、webSocket项目Nginx配置](#5webSocket项目Nginx配置)
62 | - [5.1 为什么要配置Nginx](#51-为什么要配置Nginx)
63 | - [5.2 nginx配置](#52-nginx配置)
64 | - [5.3 问题处理](#53-问题处理)
65 | - [6、压测](#6压测)
66 | - [6.1 Linux内核优化](#61-Linux内核优化)
67 | - [6.2 压测准备](#62-压测准备)
68 | - [6.3 压测数据](#63-压测数据)
69 | - [7、如何基于webSocket实现一个分布式Im](#7如何基于webSocket实现一个分布式Im)
70 | - [7.1 说明](#71-说明)
71 | - [7.2 架构](#72-架构)
72 | - [7.3 分布式系统部署](#73-分布式系统部署)
73 | - [8、回顾和反思](#8回顾和反思)
74 | - [8.1 在其它系统应用](#81-在其它系统应用)
75 | - [8.2 需要完善、优化](#82-需要完善优化)
76 | - [8.3 总结](#83-总结)
77 | - [9、参考文献](#9参考文献)
78 |
79 |
80 | ## 1、项目说明
81 | #### 1.1 goWebSocket
82 |
83 | 本文将介绍如何实现一个基于websocket聊天(IM)分布式系统。
84 |
85 | 使用golang实现websocket通讯,单机支持百万连接,使用gin框架、nginx负载、可以水平部署、程序内部相互通讯、使用grpc通讯协议。
86 |
87 | - 一般项目中webSocket使用的架构图
88 | 
89 |
90 | #### 1.2 项目体验
91 | - [项目地址 gowebsocket](https://github.com/link1st/gowebsocket)
92 | - [IM-聊天首页](http://im.20jd.com/home/index) 或者在新的窗口打开 http://im.20jd.com/home/index
93 | - 打开连接以后进入聊天界面
94 | - 多人群聊可以同时打开两个窗口
95 |
96 | ## 2、介绍webSocket
97 | ### 2.1 webSocket 是什么
98 | WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
99 |
100 | 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
101 |
102 | - HTTP和WebSocket在通讯过程的比较
103 | 
104 |
105 | - HTTP和webSocket都支持配置证书,`ws://` 无证书 `wss://` 配置证书的协议标识
106 | 
107 |
108 | ### 2.2 webSocket的兼容性
109 | - 浏览器的兼容性,开始支持webSocket的版本
110 |
111 | 
112 |
113 | - 服务端的支持
114 |
115 | golang、java、php、node.js、python、nginx 都有不错的支持
116 |
117 | - Android和IOS的支持
118 |
119 | Android可以使用java-webSocket对webSocket支持
120 |
121 | iOS 4.2及更高版本具有WebSockets支持
122 |
123 | ### 2.3 为什么要用webSocket
124 | - 1. 从业务上出发,需要一个主动通达客户端的能力
125 | > 目前大多数的请求都是使用HTTP,都是由客户端发起一个请求,有服务端处理,然后返回结果,不可以服务端主动向某一个客户端主动发送数据
126 |
127 | 
128 | - 2. 大多数场景我们需要主动通知用户,如:聊天系统、用户完成任务主动告诉用户、一些运营活动需要通知到在线的用户
129 | - 3. 可以获取用户在线状态
130 | - 4. 在没有长连接的时候通过客户端主动轮询获取数据
131 | - 5. 可以通过一种方式实现,多种不同平台(H5/Android/IOS)去使用
132 |
133 | ### 2.4 webSocket建立过程
134 | - 1. 客户端先发起升级协议的请求
135 |
136 | 客户端发起升级协议的请求,采用标准的HTTP报文格式,在报文中添加头部信息
137 |
138 | `Connection: Upgrade`表明连接需要升级
139 |
140 | `Upgrade: websocket`需要升级到 websocket协议
141 |
142 | `Sec-WebSocket-Version: 13` 协议的版本为13
143 |
144 | `Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA==` 这个是base64 encode 的值,是浏览器随机生成的,与服务器响应的 `Sec-WebSocket-Accept`对应
145 |
146 | ```
147 | # Request Headers
148 | Connection: Upgrade
149 | Host: im.20jd.com
150 | Origin: http://im.20jd.com
151 | Pragma: no-cache
152 | Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
153 | Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA==
154 | Sec-WebSocket-Version: 13
155 | Upgrade: websocket
156 | ```
157 |
158 | 
159 |
160 | - 2. 服务器响应升级协议
161 |
162 | 服务端接收到升级协议的请求,如果服务端支持升级协议会做如下响应
163 |
164 | 返回:
165 |
166 | `Status Code: 101 Switching Protocols` 表示支持切换协议
167 |
168 | ```
169 | # Response Headers
170 | Connection: upgrade
171 | Date: Fri, 09 Aug 2019 07:36:59 GMT
172 | Sec-WebSocket-Accept: mB5emvxi2jwTUhDdlRtADuBax9E=
173 | Server: nginx/1.12.1
174 | Upgrade: websocket
175 | ```
176 |
177 | - 3. 升级协议完成以后,客户端和服务器就可以相互发送数据
178 |
179 | 
180 |
181 | ## 3、如何实现基于webSocket的长连接系统
182 |
183 | ### 3.1 使用go实现webSocket服务端
184 |
185 | #### 3.1.1 启动端口监听
186 | - websocket需要监听端口,所以需要在`golang` 程序的 `main` 函数中用协程的方式去启动程序
187 | - **main.go** 实现启动
188 |
189 | ```
190 | go websocket.StartWebSocket()
191 | ```
192 | - **init_acc.go** 启动程序
193 |
194 | ```
195 | // 启动程序
196 | func StartWebSocket() {
197 | http.HandleFunc("/acc", wsPage)
198 | http.ListenAndServe(":8089", nil)
199 | }
200 | ```
201 |
202 | #### 3.1.2 升级协议
203 | - 客户端是通过http请求发送到服务端,我们需要对http协议进行升级为websocket协议
204 | - 对http请求协议进行升级 golang 库[gorilla/websocket](https://github.com/gorilla/websocket) 已经做得很好了,我们直接使用就可以了
205 | - 在实际使用的时候,建议每个连接使用两个协程处理客户端请求数据和向客户端发送数据,虽然开启协程会占用一些内存,但是读取分离,减少收发数据堵塞的可能
206 | - **init_acc.go**
207 |
208 | ```
209 | func wsPage(w http.ResponseWriter, req *http.Request) {
210 |
211 | // 升级协议
212 | conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
213 | fmt.Println("升级协议", "ua:", r.Header["User-Agent"], "referer:", r.Header["Referer"])
214 |
215 | return true
216 | }}).Upgrade(w, req, nil)
217 | if err != nil {
218 | http.NotFound(w, req)
219 |
220 | return
221 | }
222 |
223 | fmt.Println("webSocket 建立连接:", conn.RemoteAddr().String())
224 |
225 | currentTime := uint64(time.Now().Unix())
226 | client := NewClient(conn.RemoteAddr().String(), conn, currentTime)
227 |
228 | go client.read()
229 | go client.write()
230 |
231 | // 用户连接事件
232 | clientManager.Register <- client
233 | }
234 | ```
235 |
236 | #### 3.1.3 客户端连接的管理
237 | - 当前程序有多少用户连接,还需要对用户广播的需要,这里我们就需要一个管理者(clientManager),处理这些事件:
238 | - 记录全部的连接、登录用户的可以通过 **appID+uuid** 查到用户连接
239 | - 使用map存储,就涉及到多协程并发读写的问题,所以需要加读写锁
240 | - 定义四个channel ,分别处理客户端建立连接、用户登录、断开连接、全员广播事件
241 |
242 | ```
243 | // 连接管理
244 | type ClientManager struct {
245 | Clients map[*Client]bool // 全部的连接
246 | ClientsLock sync.RWMutex // 读写锁
247 | Users map[string]*Client // 登录的用户 // appID+uuid
248 | UserLock sync.RWMutex // 读写锁
249 | Register chan *Client // 连接连接处理
250 | Login chan *login // 用户登录处理
251 | Unregister chan *Client // 断开连接处理程序
252 | Broadcast chan []byte // 广播 向全部成员发送数据
253 | }
254 |
255 | // 初始化
256 | func NewClientManager() (clientManager *ClientManager) {
257 | clientManager = &ClientManager{
258 | Clients: make(map[*Client]bool),
259 | Users: make(map[string]*Client),
260 | Register: make(chan *Client, 1000),
261 | Login: make(chan *login, 1000),
262 | Unregister: make(chan *Client, 1000),
263 | Broadcast: make(chan []byte, 1000),
264 | }
265 |
266 | return
267 | }
268 | ```
269 |
270 | #### 3.1.4 注册客户端的socket的写的异步处理程序
271 | - 防止发生程序崩溃,所以需要捕获异常
272 | - 为了显示异常崩溃位置这里使用`string(debug.Stack())`打印调用堆栈信息
273 | - 如果写入数据失败了,可能连接有问题,就关闭连接
274 | - **client.go**
275 |
276 | ```
277 | // 向客户端写数据
278 | func (c *Client) write() {
279 | defer func() {
280 | if r := recover(); r != nil {
281 | fmt.Println("write stop", string(debug.Stack()), r)
282 |
283 | }
284 | }()
285 |
286 | defer func() {
287 | clientManager.Unregister <- c
288 | c.Socket.Close()
289 | fmt.Println("Client发送数据 defer", c)
290 | }()
291 |
292 | for {
293 | select {
294 | case message, ok := <-c.Send:
295 | if !ok {
296 | // 发送数据错误 关闭连接
297 | fmt.Println("Client发送数据 关闭连接", c.Addr, "ok", ok)
298 |
299 | return
300 | }
301 |
302 | c.Socket.WriteMessage(websocket.TextMessage, message)
303 | }
304 | }
305 | }
306 | ```
307 |
308 | #### 3.1.5 注册客户端的socket的读的异步处理程序
309 | - 循环读取客户端发送的数据并处理
310 | - 如果读取数据失败了,关闭channel
311 | - **client.go**
312 |
313 | ```
314 | // 读取客户端数据
315 | func (c *Client) read() {
316 | defer func() {
317 | if r := recover(); r != nil {
318 | fmt.Println("write stop", string(debug.Stack()), r)
319 | }
320 | }()
321 |
322 | defer func() {
323 | fmt.Println("读取客户端数据 关闭send", c)
324 | close(c.Send)
325 | }()
326 |
327 | for {
328 | _, message, err := c.Socket.ReadMessage()
329 | if err != nil {
330 | fmt.Println("读取客户端数据 错误", c.Addr, err)
331 |
332 | return
333 | }
334 |
335 | // 处理程序
336 | fmt.Println("读取客户端数据 处理:", string(message))
337 | ProcessData(c, message)
338 | }
339 | }
340 | ```
341 |
342 | #### 3.1.6 接收客户端数据并处理
343 | - 约定发送和接收请求数据格式,为了js处理方便,采用了`json`的数据格式发送和接收数据(人类可以阅读的格式在工作开发中使用是比较方便的)
344 |
345 | - 登录发送数据示例:
346 | ```
347 | {"seq":"1565336219141-266129","cmd":"login","data":{"userID":"马远","appID":101}}
348 | ```
349 | - 登录响应数据示例:
350 | ```
351 | {"seq":"1565336219141-266129","cmd":"login","response":{"code":200,"codeMsg":"Success","data":null}}
352 | ```
353 | - websocket是双向的数据通讯,可以连续发送,如果发送的数据需要服务端回复,就需要一个**seq**来确定服务端的响应是回复哪一次的请求数据
354 | - cmd 是用来确定动作,websocket没有类似于http的url,所以规定 cmd 是什么动作
355 | - 目前的动作有:login/heartbeat 用来发送登录请求和连接保活(长时间没有数据发送的长连接容易被浏览器、移动中间商、nginx、服务端程序断开)
356 | - 为什么需要AppID,UserID是表示用户的唯一字段,设计的时候为了做成通用性,设计AppID用来表示用户在哪个平台登录的(web、app、ios等),方便后续扩展
357 |
358 | - **request_model.go** 约定的请求数据格式
359 |
360 | ```
361 | /************************ 请求数据 **************************/
362 | // 通用请求数据格式
363 | type Request struct {
364 | Seq string `json:"seq"` // 消息的唯一ID
365 | Cmd string `json:"cmd"` // 请求命令字
366 | Data interface{} `json:"data,omitempty"` // 数据 json
367 | }
368 |
369 | // 登录请求数据
370 | type Login struct {
371 | ServiceToken string `json:"serviceToken"` // 验证用户是否登录
372 | AppID uint32 `json:"appID,omitempty"`
373 | UserID string `json:"userID,omitempty"`
374 | }
375 |
376 | // 心跳请求数据
377 | type HeartBeat struct {
378 | UserID string `json:"userID,omitempty"`
379 | }
380 | ```
381 |
382 | - **response_model.go**
383 |
384 | ```
385 | /************************ 响应数据 **************************/
386 | type Head struct {
387 | Seq string `json:"seq"` // 消息的ID
388 | Cmd string `json:"cmd"` // 消息的cmd 动作
389 | Response *Response `json:"response"` // 消息体
390 | }
391 |
392 | type Response struct {
393 | Code uint32 `json:"code"`
394 | CodeMsg string `json:"codeMsg"`
395 | Data interface{} `json:"data"` // 数据 json
396 | }
397 |
398 | ```
399 |
400 |
401 | #### 3.1.7 使用路由的方式处理客户端的请求数据
402 |
403 | - 使用路由的方式处理由客户端发送过来的请求数据
404 | - 以后添加请求类型以后就可以用类是用http相类似的方式(router-controller)去处理
405 | - **acc_routers.go**
406 |
407 | ```
408 | // Websocket 路由
409 | func WebsocketInit() {
410 | websocket.Register("login", websocket.LoginController)
411 | websocket.Register("heartbeat", websocket.HeartbeatController)
412 | }
413 | ```
414 |
415 | #### 3.1.8 防止内存溢出和Goroutine不回收
416 | - 1. 定时任务清除超时连接
417 | 没有登录的连接和登录的连接6分钟没有心跳则断开连接
418 |
419 | **client_manager.go**
420 |
421 | ```
422 | // 定时清理超时连接
423 | func ClearTimeoutConnections() {
424 | currentTime := uint64(time.Now().Unix())
425 |
426 | for client := range clientManager.Clients {
427 | if client.IsHeartbeatTimeout(currentTime) {
428 | fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserID, client.LoginTime, client.HeartbeatTime)
429 |
430 | client.Socket.Close()
431 | }
432 | }
433 | }
434 | ```
435 |
436 | - 2. 读写的Goroutine有一个失败,则相互关闭
437 | `write()`Goroutine写入数据失败,关闭`c.Socket.Close()`连接,会关闭`read()`Goroutine
438 | `read()`Goroutine读取数据失败,关闭`close(c.Send)`连接,会关闭`write()`Goroutine
439 |
440 | - 3. 客户端主动关闭
441 | 关闭读写的Goroutine
442 | 从`ClientManager`删除连接
443 |
444 | - 4. 监控用户连接、Goroutine数
445 | 十个内存溢出有九个和Goroutine有关
446 | 添加一个http的接口,可以查看系统的状态,防止Goroutine不回收
447 | [查看系统状态](http://im.20jd.com/system/state?isDebug=true)
448 |
449 | - 5. Nginx 配置不活跃的连接释放时间,防止忘记关闭的连接
450 |
451 | - 6. 使用 pprof 分析性能、耗时
452 |
453 | ### 3.2 使用javaScript实现webSocket客户端
454 | #### 3.2.1 启动并注册监听程序
455 | - js 建立连接,并处理连接成功、收到数据、断开连接的事件处理
456 |
457 | ```
458 | ws = new WebSocket("ws://127.0.0.1:8089/acc");
459 |
460 |
461 | ws.onopen = function(evt) {
462 | console.log("Connection open ...");
463 | };
464 |
465 | ws.onmessage = function(evt) {
466 | console.log( "Received Message: " + evt.data);
467 | data_array = JSON.parse(evt.data);
468 | console.log( data_array);
469 | };
470 |
471 | ws.onclose = function(evt) {
472 | console.log("Connection closed.");
473 | };
474 |
475 | ```
476 |
477 |
478 | #### 3.2.2 发送数据
479 | - 需要注意:连接建立成功以后才可以发送数据
480 | - 建立连接以后由客户端向服务器发送数据示例
481 |
482 | ```
483 | 登录:
484 | ws.send('{"seq":"2323","cmd":"login","data":{"userID":"11","appID":101}}');
485 |
486 | 心跳:
487 | ws.send('{"seq":"2324","cmd":"heartbeat","data":{}}');
488 |
489 | ping 查看服务是否正常:
490 | ws.send('{"seq":"2325","cmd":"ping","data":{}}');
491 |
492 | 关闭连接:
493 | ws.close();
494 | ```
495 |
496 | ## 3.3 发送消息
497 | ### 3.3.1 文本消息
498 |
499 | 客户端只要知道发送用户是谁,还有内容就可以显示文本消息,这里我们重点关注一下数据部分
500 |
501 | target:定义接收的目标,目前未设置
502 |
503 | type:消息的类型,text 文本消息 img 图片消息
504 |
505 | msg:文本消息内容
506 |
507 | from:消息的发送者
508 |
509 | 文本消息的结构:
510 |
511 | ```json
512 | {
513 | "seq": "1569080188418-747717",
514 | "cmd": "msg",
515 | "response": {
516 | "code": 200,
517 | "codeMsg": "Ok",
518 | "data": {
519 | "target": "",
520 | "type": "text",
521 | "msg": "hello",
522 | "from": "马超"
523 | }
524 | }
525 | }
526 | ```
527 |
528 | 这样一个文本消息的结构就设计完成了,客户端在接收到消息内容就可以展现到 IM 界面上
529 |
530 | ### 3.3.2 图片和语言消息
531 |
532 | 发送图片消息,发送消息者的客户端需要先把图片上传到文件服务器,上传成功以后获得图片访问的 URL,然后由发送消息者的客户端需要将图片 URL 发送到 gowebsocket,gowebsocket 图片的消息格式发送给目标客户端,消息接收者客户端接收到图片的 URL 就可以显示图片消息。
533 |
534 | 图片消息的结构:
535 |
536 | ```
537 | {
538 | "type": "img",
539 | "from": "马超",
540 | "url": "http://20jd.com/images/home_logo.png",
541 | "secret": "消息鉴权 secret",
542 | "size": {
543 | "width": 480,
544 | "height": 720
545 | }
546 | }
547 | ```
548 |
549 | 语言消息、和视频消息和图片消息类似,都是先把文件上传服务器,然后通过 gowebsocket 传递文件的 URL,需要注意的是部分消息涉及到隐私的文件,文件访问的时候需要做好鉴权信息,不能让非接收用户也能查看到别人的消息内容。
550 |
551 | ## 4、goWebSocket 项目
552 | ### 4.1 项目说明
553 | - 本项目是基于webSocket实现的分布式IM系统
554 | - 客户端随机分配用户名,所有人进入一个聊天室,实现群聊的功能
555 | - 单台机器(24核128G内存)支持百万客户端连接
556 | - 支持水平部署,部署的机器之间可以相互通讯
557 |
558 | - 项目架构图
559 | 
560 |
561 | ### 4.2 项目依赖
562 |
563 | - 本项目只需要使用 redis 和 golang
564 | - 本项目使用govendor管理依赖,克隆本项目就可以直接使用
565 |
566 | ```
567 | # 主要使用到的包
568 | github.com/gin-gonic/gin@v1.4.0
569 | github.com/redis/go-redis/v9
570 | github.com/gorilla/websocket
571 | github.com/spf13/viper
572 | google.golang.org/grpc
573 | github.com/golang/protobuf
574 | ```
575 |
576 |
577 | ### 4.3 项目启动
578 | - 克隆项目
579 |
580 | ```
581 | git clone git@github.com:link1st/gowebsocket.git
582 | # 或
583 | git clone https://github.com/link1st/gowebsocket.git
584 | ```
585 | - 修改项目配置
586 |
587 | ```
588 | cd gowebsocket
589 | cd config
590 | mv app.yaml.example app.yaml
591 | # 修改项目监听端口,redis连接等(默认127.0.0.1:3306)
592 | vim app.yaml
593 | # 返回项目目录,为以后启动做准备
594 | cd ..
595 | ```
596 | - 配置文件说明
597 |
598 | ```
599 | app:
600 | logFile: log/gin.log # 日志文件位置
601 | httpPort: 8080 # http端口
602 | webSocketPort: 8089 # webSocket端口
603 | rpcPort: 9001 # 分布式部署程序内部通讯端口
604 | httpUrl: 127.0.0.1:8080
605 | webSocketUrl: 127.0.0.1:8089
606 |
607 |
608 | redis:
609 | addr: "localhost:6379"
610 | password: ""
611 | DB: 0
612 | poolSize: 30
613 | minIDleConns: 30
614 | ```
615 |
616 | - 启动项目
617 |
618 | ```
619 | go run main.go
620 | ```
621 |
622 | - 进入IM聊天地址
623 | [http://127.0.0.1:8080/home/index](http://127.0.0.1:8080/home/index)
624 | - 到这里,就可以体验到基于webSocket的IM系统
625 |
626 | #### 4.4 接口文档
627 | ###### 4.4.1.1 接口说明
628 | ##### 4.4.1 HTTP接口文档
629 | - 在接口开发和接口文档使用的过程中,规范开发流程,减少沟通成本,所以约定一下接口开发流程和文档说明
630 | - 接口地址
631 |
632 | 线上:http://im.20jd.com
633 |
634 | 测试:http://im.20jd.com
635 |
636 |
637 | ###### 4.4.1.2 聊天页面
638 | - 地址:/home/index
639 | - 请求方式:GET
640 | - 接口说明:聊天页面
641 | - 请求参数:
642 |
643 | | 参数 | 必填 | 类型 | 说明 | 示例 |
644 | | :----: | :----: | :----: | :----: | :----: |
645 | | appID | 是 | uint32 | appID/房间ID | 101 |
646 |
647 | - 返回参数:
648 | 无
649 |
650 |
651 | ###### 4.4.1.3 获取房间用户列表
652 | - 地址:/user/list
653 | - 请求方式:GET/POST
654 | - 接口说明:获取房间用户列表
655 | - 请求参数:
656 |
657 | | 参数 | 必填 | 类型 | 说明 | 示例 |
658 | | :----: | :----: | :----: | :----: | :----: |
659 | | appID | 是 | uint32 | appID/房间ID | 101 |
660 |
661 | - 返回参数:
662 |
663 | | 参数 | 必填 | 类型 | 说明 | 示例 |
664 | | :----: | :----: | :----: | :----: | :----: |
665 | | code | 是 | int | 错误码 | 200 |
666 | | msg | 是 | string| 错误信息 |Success |
667 | | data | 是 | array | 返回数据 | |
668 | | userCount | 是 | int | 房间内用户总数 | 1 |
669 | | userList| 是 | list | 用户列表 | |
670 |
671 | - 示例:
672 |
673 | ```json
674 | {
675 | "code": 200,
676 | "msg": "Success",
677 | "data": {
678 | "userCount": 1,
679 | "userList": [
680 | "黄帝"
681 | ]
682 | }
683 | }
684 | ```
685 |
686 | ###### 4.4.1.4 查询用户是否在线
687 | - 地址:/user/online
688 | - 请求方式:GET/POST
689 | - 接口说明:查询用户是否在线
690 | - 请求参数:
691 |
692 | | 参数 | 必填 | 类型 | 说明 | 示例 |
693 | | :----: | :----: | :----: | :----: | :----: |
694 | | appID | 是 | uint32 | appID/房间ID | 101 |
695 | | userID | 是 | string | 用户ID | 黄帝 |
696 |
697 | - 返回参数:
698 |
699 | | 参数 | 必填 | 类型 | 说明 | 示例 |
700 | | :----: | :----: | :----: | :----: | :----: |
701 | | code | 是 | int | 错误码 | 200 |
702 | | msg | 是 | string| 错误信息 |Success |
703 | | data | 是 | array | 返回数据 | |
704 | | online | 是 | bool | 发送结果 true:在线 false:不在线 | true |
705 | | userID | 是 | string | 用户ID | 黄帝 |
706 |
707 | - 示例:
708 |
709 | ```json
710 | {
711 | "code": 200,
712 | "msg": "Success",
713 | "data": {
714 | "online": true,
715 | "userID": "黄帝"
716 | }
717 | }
718 | ```
719 |
720 | ###### 4.4.1.5 给用户发送消息
721 | - 地址:/user/sendMessage
722 | - 请求方式:GET/POST
723 | - 接口说明:给用户发送消息
724 | - 请求参数:
725 |
726 | | 参数 | 必填 | 类型 | 说明 | 示例 |
727 | | :----: | :----: | :----: | :----: | :----: |
728 | | appID | 是 | uint32 | appID/房间ID | 101 |
729 | | userID | 是 | string | 用户id | 黄帝 |
730 | | msgID | 是 | string | 消息ID | 避免重复发送 |
731 | | message | 是 | string | 消息内容 | hello |
732 |
733 | - 返回参数:
734 |
735 | | 参数 | 必填 | 类型 | 说明 | 示例 |
736 | | :----: | :----: | :----: | :----: | :----: |
737 | | code | 是 | int | 错误码 | 200 |
738 | | msg | 是 | string| 错误信息 |Success |
739 | | data | 是 | array | 返回数据 | |
740 | | sendResults | 是 | bool | 发送结果 true:成功 false:失败 | true |
741 |
742 | - 示例:
743 |
744 | ```json
745 | {
746 | "code": 200,
747 | "msg": "Success",
748 | "data": {
749 | "sendResults": true
750 | }
751 | }
752 | ```
753 |
754 | ###### 4.4.1.6 给全员用户发送消息
755 | - 地址:/user/sendMessageAll
756 | - 请求方式:GET/POST
757 | - 接口说明:给全员用户发送消息
758 | - 请求参数:
759 |
760 | | 参数 | 必填 | 类型 | 说明 | 示例 |
761 | | :----: | :----: | :----: | :----: | :----: |
762 | | appID | 是 | uint32 | appID/房间ID | 101 |
763 | | userID | 是 | string | 用户id | 黄帝 |
764 | | msgID | 是 | string | 消息ID | 避免重复发送 |
765 | | message | 是 | string | 消息内容 | hello |
766 |
767 | - 返回参数:
768 |
769 | | 参数 | 必填 | 类型 | 说明 | 示例 |
770 | | :----: | :----: | :----: | :----: | :----: |
771 | | code | 是 | int | 错误码 | 200 |
772 | | msg | 是 | string| 错误信息 |Success |
773 | | data | 是 | array | 返回数据 | |
774 | | sendResults | 是 | bool | 发送结果 true:成功 false:失败 | true |
775 |
776 | - 示例:
777 |
778 | ```json
779 | {
780 | "code": 200,
781 | "msg": "Success",
782 | "data": {
783 | "sendResults": true
784 | }
785 | }
786 | ```
787 |
788 | ##### 4.4.2 RPC接口文档
789 | ###### 4.4.2.1 接口说明
790 | - 接口协议结构体
791 | ```proto
792 | syntax = "proto3";
793 |
794 | option java_multiple_files = true;
795 | option java_package = "io.grpc.examples.protobuf";
796 | option java_outer_classname = "ProtobufProto";
797 |
798 |
799 | package protobuf;
800 |
801 | // The AccServer service definition.
802 | service AccServer {
803 | // 查询用户是否在线
804 | rpc QueryUsersOnline (QueryUsersOnlineReq) returns (QueryUsersOnlineRsp) {
805 | }
806 | // 发送消息
807 | rpc SendMsg (SendMsgReq) returns (SendMsgRsp) {
808 | }
809 | // 给这台机器的房间内所有用户发送消息
810 | rpc SendMsgAll (SendMsgAllReq) returns (SendMsgAllRsp) {
811 | }
812 | // 获取用户列表
813 | rpc GetUserList (GetUserListReq) returns (GetUserListRsp) {
814 | }
815 | }
816 |
817 | // 查询用户是否在线
818 | message QueryUsersOnlineReq {
819 | uint32 appID = 1; // AppID
820 | string userID = 2; // 用户ID
821 | }
822 |
823 | message QueryUsersOnlineRsp {
824 | uint32 retCode = 1;
825 | string errMsg = 2;
826 | bool online = 3;
827 | }
828 |
829 | // 发送消息
830 | message SendMsgReq {
831 | string seq = 1; // 序列号
832 | uint32 appID = 2; // appID/房间ID
833 | string userID = 3; // 用户ID
834 | string cms = 4; // cms 动作: msg/enter/exit
835 | string type = 5; // type 消息类型,默认是 text
836 | string msg = 6; // msg
837 | bool isLocal = 7; // 是否查询本机 acc内部调用为:true(本机查询不到即结束)
838 | }
839 |
840 | message SendMsgRsp {
841 | uint32 retCode = 1;
842 | string errMsg = 2;
843 | string sendMsgID = 3;
844 | }
845 |
846 | // 给这台机器的房间内所有用户发送消息
847 | message SendMsgAllReq {
848 | string seq = 1; // 序列号
849 | uint32 appID = 2; // appID/房间ID
850 | string userID = 3; // 不发送的用户ID
851 | string cms = 4; // cms 动作: msg/enter/exit
852 | string type = 5; // type 消息类型,默认是 text
853 | string msg = 6; // msg
854 | }
855 |
856 | message SendMsgAllRsp {
857 | uint32 retCode = 1;
858 | string errMsg = 2;
859 | string sendMsgID = 3;
860 | }
861 |
862 | // 获取用户列表
863 | message GetUserListReq {
864 | uint32 appID = 1;
865 | }
866 |
867 | message GetUserListRsp {
868 | uint32 retCode = 1;
869 | string errMsg = 2;
870 | repeated string userID = 3;
871 | }
872 | ```
873 |
874 | ###### 4.4.2.2 查询用户是否在线
875 | - 参考上述协议结构体
876 |
877 | ###### 4.4.2.3 发送消息
878 | ###### 4.4.2.4 给指定房间所有用户发送消息
879 | ###### 4.4.2.5 获取房间内全部用户
880 |
881 | ## 5、webSocket项目Nginx配置
882 | ### 5.1 为什么要配置Nginx
883 | - 使用nginx实现内外网分离,对外只暴露Nginx的Ip(一般的互联网企业会在nginx之前加一层LVS做负载均衡),减少入侵的可能
884 | - 支持配置 ssl 证书,使用 `wss` 的方式实现数据加密,减少数据被抓包和篡改的可能性
885 | - 使用Nginx可以利用Nginx的负载功能,前端再使用的时候只需要连接固定的域名,通过Nginx将流量分发了到不同的机器
886 | - 同时我们也可以使用Nginx的不同的负载策略(轮询、weight、ip_hash)
887 |
888 | ### 5.2 nginx配置
889 | - 使用域名 **im.20jd.com** 为示例,参考配置
890 | - 一级目录**im.20jd.com/acc** 是给webSocket使用,是用nginx stream转发功能(nginx 1.3.31 开始支持,使用Tengine配置也是相同的),转发到golang 8089 端口处理
891 | - 其它目录是给HTTP使用,转发到golang 8080 端口处理
892 |
893 | ```
894 | upstream go-im
895 | {
896 | server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
897 | keepalive 16;
898 | }
899 |
900 | upstream go-acc
901 | {
902 | server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
903 | keepalive 16;
904 | }
905 |
906 |
907 | server {
908 | listen 80 ;
909 | server_name im.20jd.com;
910 | index index.html index.htm ;
911 |
912 |
913 | location /acc {
914 | proxy_set_header Host $host;
915 | proxy_pass http://go-acc;
916 | proxy_http_version 1.1;
917 | proxy_set_header Upgrade $http_upgrade;
918 | proxy_set_header Connection $connection_upgrade;
919 | proxy_set_header Connection "";
920 | proxy_redirect off;
921 | proxy_intercept_errors on;
922 | client_max_body_size 10m;
923 | }
924 |
925 | location /
926 | {
927 | proxy_set_header Host $host;
928 | proxy_pass http://go-im;
929 | proxy_http_version 1.1;
930 | proxy_set_header Connection "";
931 | proxy_redirect off;
932 | proxy_intercept_errors on;
933 | client_max_body_size 30m;
934 | }
935 |
936 | access_log /link/log/nginx/access/im.log;
937 | error_log /link/log/nginx/access/im.error.log;
938 | }
939 | ```
940 |
941 | ### 5.3 问题处理
942 | - 运行nginx测试命令,查看配置文件是否正确
943 |
944 | ```
945 | /link/server/tengine/sbin/nginx -t
946 |
947 | ```
948 |
949 | - 如果出现错误
950 |
951 | ```
952 | nginx: [emerg] unknown "connection_upgrade" variable
953 | configuration file /link/server/tengine/conf/nginx.conf test failed
954 | ```
955 |
956 | - 处理方法
957 | - 在**nginx.com**添加
958 |
959 | ```
960 | http{
961 | fastcgi_temp_file_write_size 128k;
962 | ..... # 需要添加的内容
963 |
964 | #support websocket
965 | map $http_upgrade $connection_upgrade {
966 | default upgrade;
967 | '' close;
968 | }
969 |
970 | .....
971 | gzip on;
972 |
973 | }
974 |
975 | ```
976 |
977 | - 原因:Nginx代理webSocket的时候就会遇到Nginx的设计问题 **End-to-end and Hop-by-hop Headers**
978 |
979 |
980 | ## 6、压测
981 | ### 6.1 Linux内核优化
982 | - 设置文件打开句柄数
983 |
984 | 被压测服务器需要保持100W长连接,客户和服务器端是通过socket通讯的,每个连接需要建立一个socket,程序需要保持100W长连接就需要单个程序能打开100W个文件句柄
985 |
986 | ```
987 | # 查看系统默认的值
988 | ulimit -n
989 | # 设置最大打开文件数
990 | ulimit -n 1000000
991 | ```
992 |
993 | 通过修改配置文件的方式修改程序最大打开句柄数
994 |
995 | ```
996 | root soft nofile 1040000
997 | root hard nofile 1040000
998 |
999 | root soft nofile 1040000
1000 | root hard nproc 1040000
1001 |
1002 | root soft core unlimited
1003 | root hard core unlimited
1004 |
1005 | * soft nofile 1040000
1006 | * hard nofile 1040000
1007 |
1008 | * soft nofile 1040000
1009 | * hard nproc 1040000
1010 |
1011 | * soft core unlimited
1012 | * hard core unlimited
1013 | ```
1014 |
1015 | 修改完成以后需要重启机器配置才能生效
1016 |
1017 | - 修改系统级别文件句柄数量
1018 |
1019 | file-max的值需要大于limits设置的值
1020 |
1021 | ```
1022 | # file-max 设置的值参考
1023 | cat /proc/sys/fs/file-max
1024 | 12553500
1025 | ```
1026 |
1027 | - 设置sockets连接参数
1028 |
1029 | `vim /etc/sysctl.conf`
1030 |
1031 | ```
1032 | # 配置参考
1033 | net.ipv4.tcp_tw_reuse = 1
1034 | net.ipv4.tcp_tw_recycle = 0
1035 | net.ipv4.ip_local_port_range = 1024 65000
1036 | net.ipv4.tcp_mem = 786432 2097152 3145728
1037 | net.ipv4.tcp_rmem = 4096 4096 16777216
1038 | net.ipv4.tcp_wmem = 4096 4096 16777216
1039 | ```
1040 |
1041 | `sysctl -p` 修改配置以后使得配置生效命令
1042 |
1043 | ### 6.2 压测准备
1044 | - 待压测,如果大家有压测的结果欢迎补充
1045 | - 后续会出专门的教程,从申请机器、写压测用例、内核优化、得出压测数据
1046 |
1047 | - **关于压测请移步**
1048 | - [go实现的压测工具【单台机器100w连接压测实战】](https://github.com/link1st/go-stress-testing)
1049 | - 用go语言实现一款压测工具,然后对本项目进行压测,实现单台机器100W长连接
1050 |
1051 | ### 6.3 压测数据
1052 | - 项目在实际使用的时候,每个连接约占 27Kb内存
1053 | - 支持百万连接需要25G内存,单台机器实现百万长连接是可以实现的
1054 |
1055 | - 记录内存使用情况,分别记录了1W到100W连接数内存使用情况
1056 |
1057 | | 连接数 | 内存 |
1058 | | :----: | :----:|
1059 | | 10000 | 281M |
1060 | | 100000 | 2.7g |
1061 | | 200000 | 5.4g |
1062 | | 500000 | 13.1g |
1063 | | 1000000 | 25.8g |
1064 |
1065 | - [压测详细数据](https://github.com/link1st/go-stress-testing#65-%E5%8E%8B%E6%B5%8B%E6%95%B0%E6%8D%AE)
1066 |
1067 | ## 7、如何基于webSocket实现一个分布式Im
1068 | ### 7.1 说明
1069 | - 参考本项目源码
1070 | - [gowebsocket v1.0.0 单机版Im系统](https://github.com/link1st/gowebsocket/tree/v1.0.0)
1071 | - [gowebsocket v2.0.0 分布式Im系统](https://github.com/link1st/gowebsocket/tree/v2.0.0)
1072 |
1073 | - 为了方便演示,IM系统和webSocket(acc)系统合并在一个系统中
1074 | - IM系统接口:
1075 | 获取全部在线的用户,查询当前服务的全部用户+集群中服务的全部用户
1076 | 发送消息,这里采用的是http接口发送(微信网页版发送消息也是http接口),这里考虑主要是两点:
1077 | 1.服务分离,让acc系统尽量的简单一点,不掺杂其它业务逻辑
1078 | 2.发送消息是走http接口,不使用webSocket连接,采用收和发送数据分离的方式,可以加快收发数据的效率
1079 |
1080 | ### 7.2 架构
1081 |
1082 | - 项目启动注册和用户连接时序图
1083 |
1084 | 
1085 |
1086 | - 其它系统(IM、任务)向webSocket(acc)系统连接的用户发送消息时序图
1087 |
1088 | 
1089 |
1090 | ### 7.3 分布式系统部署
1091 | - 用水平部署两个项目(gowebsocket和gowebsocket1)演示分部署
1092 | - 项目之间如何相互通讯:项目启动以后将项目Ip、rpcPort注册到redis中,让其它项目可以发现,需要通讯的时候使用gRpc进行通讯
1093 | - gowebsocket
1094 |
1095 | ```
1096 | # app.yaml 配置文件信息
1097 | app:
1098 | logFile: log/gin.log
1099 | httpPort: 8080
1100 | webSocketPort: 8089
1101 | rpcPort: 9001
1102 | httpUrl: im.20jd.com
1103 | webSocketUrl: im.20jd.com
1104 |
1105 | # 在启动项目
1106 | go run main.go
1107 |
1108 | ```
1109 |
1110 | - gowebsocket1
1111 |
1112 | ```
1113 | # 将第一个项目拷贝一份
1114 | cp -rf gowebsocket gowebsocket1
1115 | # app.yaml 修改配置文件
1116 | app:
1117 | logFile: log/gin.log
1118 | httpPort: 8081
1119 | webSocketPort: 8090
1120 | rpcPort: 9002
1121 | httpUrl: im.20jd.com
1122 | webSocketUrl: im.20jd.com
1123 |
1124 | # 在启动第二个项目
1125 | go run main.go
1126 | ```
1127 |
1128 | - Nginx配置
1129 |
1130 | 在之前Nginx配置项中添加第二台机器的Ip和端口
1131 |
1132 | ```
1133 | upstream go-im
1134 | {
1135 | server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
1136 | server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=10s;
1137 | keepalive 16;
1138 | }
1139 |
1140 | upstream go-acc
1141 | {
1142 | server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
1143 | server 127.0.0.1:8090 weight=1 max_fails=2 fail_timeout=10s;
1144 | keepalive 16;
1145 | }
1146 | ```
1147 |
1148 | - 配置完成以后重启Nginx
1149 | - 重启以后请求,验证是否符合预期:
1150 |
1151 | 查看请求是否落在两个项目上
1152 | 实验两个用户分别连接不同的项目(gowebsocket和gowebsocket1)是否也可以相互发送消息
1153 |
1154 | - 关于分布式部署
1155 |
1156 | 本项目只是演示了这个项目如何分布式部署,以及分布式部署以后模块如何进行相互通讯
1157 | 完全解决系统没有单点的故障,还需 Nginx集群、redis cluster等
1158 |
1159 |
1160 | ## 8、回顾和反思
1161 | ### 8.1 在其它系统应用
1162 | - 本系统设计的初衷就是:和客户端保持一个长连接、对外部系统两个接口(查询用户是否在线、给在线的用户推送消息),实现业务的分离
1163 | - 只有和业务分离可,才可以供多个业务使用,而不是每个业务都建立一个长连接
1164 |
1165 | #### 8.2 已经实现的功能
1166 |
1167 | - gin log日志(请求日志+debug日志)
1168 | - 读取配置文件 完成
1169 | - 定时脚本,清理过期未心跳连接 完成
1170 | - http接口,获取登录、连接数量 完成
1171 | - http接口,发送push、查询有多少人在线 完成
1172 | - grpc 程序内部通讯,发送消息 完成
1173 | - appIDs 一个用户在多个平台登录
1174 | - 界面,把所有在线的人拉倒一个群里面,发送消息 完成
1175 | - ~~单聊~~、群聊 完成
1176 | - 实现分布式,水平扩张 完成
1177 | - 压测脚本
1178 | - 文档整理
1179 | - 文档目录、百万长连接的实现、为什么要实现一个IM、怎么实现一个Im
1180 | - 架构图以及扩展
1181 |
1182 | IM实现细节:
1183 |
1184 | - 定义文本消息结构 完成
1185 | - html发送文本消息 完成
1186 | - 接口接收文本消息并发送给全体 完成
1187 | - html接收到消息 显示到界面 完成
1188 | - 界面优化 需要持续优化
1189 | - 有人加入以后广播全体 完成
1190 | - 定义加入聊天室的消息结构 完成
1191 | - 引入机器人 待定
1192 |
1193 | ### 8.2 需要完善、优化
1194 | - 登录,使用微信登录 获取昵称、头像等
1195 | - 有账号系统、资料系统
1196 | - 界面优化、适配手机端
1197 | - 消息 文本消息(支持表情)、图片、语音、视频消息
1198 | - 微服务注册、发现、熔断等
1199 | - 添加配置项,单台机器最大连接数量
1200 |
1201 | ### 8.3 总结
1202 | - 虽然实现了一个分布式在聊天的IM,但是有很多细节没有处理(登录没有鉴权、界面还待优化等),但是可以通过这个示例可以了解到:通过WebSocket解决很多业务上需求
1203 | - 本文虽然号称单台机器能有百万长连接(内存上能满足),但是实际在场景远比这个复杂(cpu有些压力),当然了如果你有这么大的业务量可以购买更多的机器更好的去支撑你的业务,本程序只是演示如何在实际工作用使用webSocket.
1204 | - 参考本文,你可以实现出来符合你需要的程序
1205 |
1206 | ### 9、参考文献
1207 |
1208 | [维基百科 WebSocket](https://zh.wikipedia.org/wiki/WebSocket)
1209 |
1210 | [阮一峰 WebSocket教程](http://www.ruanyifeng.com/blog/2017/05/websocket.html)
1211 |
1212 | [WebSocket协议:5分钟从入门到精通](https://www.cnblogs.com/chyingp/p/websocket-deep-in.html)
1213 |
1214 | [go-stress-testing 单台机器100w连接压测实战](https://github.com/link1st/go-stress-testing)
1215 |
1216 | github 搜:link1st 查看项目 gowebsocket
1217 |
1218 | [https://github.com/link1st/gowebsocket](https://github.com/link1st/gowebsocket)
1219 |
1220 | ### 意见反馈
1221 |
1222 | - 在项目中遇到问题可以直接在这里找找答案或者提问 [issues](https://github.com/link1st/gowebsocket/issues)
1223 | - 也可以添加我的微信(申请信息填写:公司、姓名,我好备注下),直接反馈给我
1224 |
1225 |
1226 |
1227 |
1228 |
1229 | ### 赞助商
1230 |
1231 | - 感谢[JetBrains](https://www.jetbrains.com/?from=gowebsocket)对本项目的支持!
1232 |
1233 |
1234 |
1235 |
1236 |
1237 |
1238 |
--------------------------------------------------------------------------------
/common/error_code.go:
--------------------------------------------------------------------------------
1 | // Package common 通用函数
2 | package common
3 |
4 | const (
5 | OK = 200 // Success
6 | NotLoggedIn = 1000 // 未登录
7 | ParameterIllegal = 1001 // 参数不合法
8 | UnauthorizedUserID = 1002 // 非法的用户 ID
9 | Unauthorized = 1003 // 未授权
10 | ServerError = 1004 // 系统错误
11 | NotData = 1005 // 没有数据
12 | ModelAddError = 1006 // 添加错误
13 | ModelDeleteError = 1007 // 删除错误
14 | ModelStoreError = 1008 // 存储错误
15 | OperationFailure = 1009 // 操作失败
16 | RoutingNotExist = 1010 // 路由不存在
17 | )
18 |
19 | // GetErrorMessage 根据错误码 获取错误信息
20 | func GetErrorMessage(code uint32, message string) string {
21 | var codeMessage string
22 | codeMap := map[uint32]string{
23 | OK: "Success",
24 | NotLoggedIn: "未登录",
25 | ParameterIllegal: "参数不合法",
26 | UnauthorizedUserID: "非法的用户ID",
27 | Unauthorized: "未授权",
28 | NotData: "没有数据",
29 | ServerError: "系统错误",
30 | ModelAddError: "添加错误",
31 | ModelDeleteError: "删除错误",
32 | ModelStoreError: "存储错误",
33 | OperationFailure: "操作失败",
34 | RoutingNotExist: "路由不存在",
35 | }
36 |
37 | if message == "" {
38 | if value, ok := codeMap[code]; ok {
39 | // 存在
40 | codeMessage = value
41 | } else {
42 | codeMessage = "未定义错误类型!"
43 | }
44 | } else {
45 | codeMessage = message
46 | }
47 |
48 | return codeMessage
49 | }
50 |
--------------------------------------------------------------------------------
/common/rsp_common.go:
--------------------------------------------------------------------------------
1 | // Package common 通用函数
2 | package common
3 |
4 | // JSONResult json 返回结构体
5 | type JSONResult struct {
6 | Code uint32 `json:"code"`
7 | Msg string `json:"msg"`
8 | Data interface{} `json:"data"`
9 | }
10 |
11 | // Response 响应数据结构
12 | func Response(code uint32, message string, data interface{}) JSONResult {
13 | message = GetErrorMessage(code, message)
14 | jsonMap := grantMap(code, message, data)
15 | return jsonMap
16 | }
17 |
18 | // 按照接口格式生成原数据数组
19 | func grantMap(code uint32, message string, data interface{}) JSONResult {
20 | jsonMap := JSONResult{
21 | Code: code,
22 | Msg: message,
23 | Data: data,
24 | }
25 | return jsonMap
26 | }
27 |
--------------------------------------------------------------------------------
/config/app.yaml.example:
--------------------------------------------------------------------------------
1 | app:
2 | logFile: log/gin.log
3 | httpPort: 8080
4 | webSocketPort: 8089
5 | rpcPort: 9001
6 | httpUrl: 127.0.0.1:8080
7 | webSocketUrl: 127.0.0.1:8089
8 |
9 |
10 | redis:
11 | addr: "localhost:6379"
12 | password: ""
13 | DB: 0
14 | poolSize: 30
15 | minIdleConns: 30
--------------------------------------------------------------------------------
/controllers/base_controller.go:
--------------------------------------------------------------------------------
1 | // Package controllers 控制器
2 | package controllers
3 |
4 | import (
5 | "net/http"
6 |
7 | "github.com/gin-gonic/gin"
8 |
9 | "github.com/link1st/gowebsocket/v2/common"
10 | )
11 |
12 | type BaseController struct {
13 | gin.Context
14 | }
15 |
16 | // Response 获取全部请求解析到map
17 | func Response(c *gin.Context, code uint32, msg string, data map[string]interface{}) {
18 | message := common.Response(code, msg, data)
19 | // 允许跨域
20 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
21 | c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
22 | c.Header("Access-Control-Allow-Methods",
23 | "POST, GET, OPTIONS, PUT, DELETE,UPDATE") // 服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
24 | c.Header("Access-Control-Allow-Headers",
25 | "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
26 | c.Header("Access-Control-Expose-Headers",
27 | "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
28 | c.Header("Access-Control-Allow-Credentials",
29 | "true") // 跨域请求是否需要带cookie信息 默认设置为true
30 | c.Set("content-type",
31 | "application/json") // 设置返回格式是json
32 | c.JSON(http.StatusOK, message)
33 | return
34 | }
35 |
--------------------------------------------------------------------------------
/controllers/home/home_controller.go:
--------------------------------------------------------------------------------
1 | // Package home 首页
2 | package home
3 |
4 | import (
5 | "fmt"
6 | "net/http"
7 | "strconv"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/spf13/viper"
11 |
12 | "github.com/link1st/gowebsocket/v2/servers/websocket"
13 | )
14 |
15 | // Index 聊天页面
16 | func Index(c *gin.Context) {
17 | appIDStr := c.Query("appID")
18 | appIDUint64, _ := strconv.ParseInt(appIDStr, 10, 32)
19 | appID := uint32(appIDUint64)
20 | if !websocket.InAppIDs(appID) {
21 | appID = websocket.GetDefaultAppID()
22 | }
23 | fmt.Println("http_request 聊天首页", appID)
24 | data := gin.H{
25 | "title": "聊天首页",
26 | "appID": appID,
27 | "httpUrl": viper.GetString("app.httpUrl"),
28 | "webSocketUrl": viper.GetString("app.webSocketUrl"),
29 | }
30 | c.HTML(http.StatusOK, "index.tpl", data)
31 | }
32 |
--------------------------------------------------------------------------------
/controllers/systems/system_controller.go:
--------------------------------------------------------------------------------
1 | // Package systems 系统查询
2 | package systems
3 |
4 | import (
5 | "fmt"
6 | "runtime"
7 |
8 | "github.com/gin-gonic/gin"
9 |
10 | "github.com/link1st/gowebsocket/v2/common"
11 | "github.com/link1st/gowebsocket/v2/controllers"
12 | "github.com/link1st/gowebsocket/v2/servers/websocket"
13 | )
14 |
15 | // Status 查询系统状态
16 | func Status(c *gin.Context) {
17 | isDebug := c.Query("isDebug")
18 | fmt.Println("http_request 查询系统状态", isDebug)
19 | data := make(map[string]interface{})
20 | numGoroutine := runtime.NumGoroutine()
21 | numCPU := runtime.NumCPU()
22 |
23 | // goroutine 的数量
24 | data["numGoroutine"] = numGoroutine
25 | data["numCPU"] = numCPU
26 |
27 | // ClientManager 信息
28 | data["managerInfo"] = websocket.GetManagerInfo(isDebug)
29 | controllers.Response(c, common.OK, "", data)
30 | }
31 |
--------------------------------------------------------------------------------
/controllers/user/user_controller.go:
--------------------------------------------------------------------------------
1 | // Package user 用户调用接口
2 | package user
3 |
4 | import (
5 | "fmt"
6 | "strconv"
7 |
8 | "github.com/gin-gonic/gin"
9 |
10 | "github.com/link1st/gowebsocket/v2/common"
11 | "github.com/link1st/gowebsocket/v2/controllers"
12 | "github.com/link1st/gowebsocket/v2/lib/cache"
13 | "github.com/link1st/gowebsocket/v2/models"
14 | "github.com/link1st/gowebsocket/v2/servers/websocket"
15 | )
16 |
17 | // List 查看全部在线用户
18 | func List(c *gin.Context) {
19 | appIDStr := c.Query("appID")
20 | appIDUint64, _ := strconv.ParseInt(appIDStr, 10, 32)
21 | appID := uint32(appIDUint64)
22 | fmt.Println("http_request 查看全部在线用户", appID)
23 | data := make(map[string]interface{})
24 | userList := websocket.UserList(appID)
25 | data["userList"] = userList
26 | data["userCount"] = len(userList)
27 | controllers.Response(c, common.OK, "", data)
28 | }
29 |
30 | // Online 查看用户是否在线
31 | func Online(c *gin.Context) {
32 | userID := c.Query("userID")
33 | appIDStr := c.Query("appID")
34 | appIDUint64, _ := strconv.ParseInt(appIDStr, 10, 32)
35 | appID := uint32(appIDUint64)
36 | fmt.Println("http_request 查看用户是否在线", userID, appIDStr)
37 | data := make(map[string]interface{})
38 | online := websocket.CheckUserOnline(appID, userID)
39 | data["userID"] = userID
40 | data["online"] = online
41 | controllers.Response(c, common.OK, "", data)
42 | }
43 |
44 | // SendMessage 给用户发送消息
45 | func SendMessage(c *gin.Context) {
46 | // 获取参数
47 | appIDStr := c.PostForm("appID")
48 | userID := c.PostForm("userID")
49 | msgID := c.PostForm("msgID")
50 | message := c.PostForm("message")
51 | appIDUint64, _ := strconv.ParseInt(appIDStr, 10, 32)
52 | appID := uint32(appIDUint64)
53 | fmt.Println("http_request 给用户发送消息", appIDStr, userID, msgID, message)
54 |
55 | // TODO::进行用户权限认证,一般是客户端传入TOKEN,然后检验TOKEN是否合法,通过TOKEN解析出来用户ID
56 | // 本项目只是演示,所以直接过去客户端传入的用户ID(userID)
57 | data := make(map[string]interface{})
58 | if cache.SeqDuplicates(msgID) {
59 | fmt.Println("给用户发送消息 重复提交:", msgID)
60 | controllers.Response(c, common.OK, "", data)
61 | return
62 | }
63 | sendResults, err := websocket.SendUserMessage(appID, userID, msgID, message)
64 | if err != nil {
65 | data["sendResultsErr"] = err.Error()
66 | }
67 | data["sendResults"] = sendResults
68 | controllers.Response(c, common.OK, "", data)
69 | }
70 |
71 | // SendMessageAll 给全员发送消息
72 | func SendMessageAll(c *gin.Context) {
73 | // 获取参数
74 | appIDStr := c.PostForm("appID")
75 | userID := c.PostForm("userID")
76 | msgID := c.PostForm("msgID")
77 | message := c.PostForm("message")
78 | appIDUint64, _ := strconv.ParseInt(appIDStr, 10, 32)
79 | appID := uint32(appIDUint64)
80 | fmt.Println("http_request 给全体用户发送消息", appIDStr, userID, msgID, message)
81 | data := make(map[string]interface{})
82 | if cache.SeqDuplicates(msgID) {
83 | fmt.Println("给用户发送消息 重复提交:", msgID)
84 | controllers.Response(c, common.OK, "", data)
85 | return
86 | }
87 | sendResults, err := websocket.SendUserMessageAll(appID, userID, msgID, models.MessageCmdMsg, message)
88 | if err != nil {
89 | data["sendResultsErr"] = err.Error()
90 | }
91 | data["sendResults"] = sendResults
92 | controllers.Response(c, common.OK, "", data)
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/link1st/gowebsocket/v2
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.24.0
6 |
7 | require (
8 | github.com/gin-gonic/gin v1.9.1
9 | github.com/golang/protobuf v1.5.3
10 | github.com/gorilla/websocket v1.4.2
11 | github.com/redis/go-redis/v9 v9.0.3
12 | github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07
13 | google.golang.org/grpc v1.56.3
14 | google.golang.org/protobuf v1.33.0
15 | )
16 |
17 | require (
18 | github.com/bytedance/sonic v1.9.1 // indirect
19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
20 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
22 | github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57 // indirect
23 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
24 | github.com/gin-contrib/sse v0.1.0 // indirect
25 | github.com/go-playground/locales v0.14.1 // indirect
26 | github.com/go-playground/universal-translator v0.18.1 // indirect
27 | github.com/go-playground/validator/v10 v10.14.0 // indirect
28 | github.com/goccy/go-json v0.10.2 // indirect
29 | github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d // indirect
30 | github.com/json-iterator/go v1.1.12 // indirect
31 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
32 | github.com/leodido/go-urn v1.2.4 // indirect
33 | github.com/magiconair/properties v1.8.1 // indirect
34 | github.com/mattn/go-isatty v0.0.19 // indirect
35 | github.com/mitchellh/mapstructure v1.1.2 // indirect
36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
37 | github.com/modern-go/reflect2 v1.0.2 // indirect
38 | github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5 // indirect
39 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
40 | github.com/spf13/afero v1.1.2 // indirect
41 | github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421 // indirect
42 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
43 | github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40 // indirect
44 | github.com/subosito/gotenv v1.1.1 // indirect
45 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
46 | github.com/ugorji/go/codec v1.2.11 // indirect
47 | golang.org/x/arch v0.3.0 // indirect
48 | golang.org/x/crypto v0.36.0 // indirect
49 | golang.org/x/net v0.38.0 // indirect
50 | golang.org/x/sys v0.31.0 // indirect
51 | golang.org/x/text v0.23.0 // indirect
52 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
53 | gopkg.in/yaml.v2 v2.4.0 // indirect
54 | gopkg.in/yaml.v3 v3.0.1 // indirect
55 | )
56 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
10 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
11 | github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
12 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
13 | github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
14 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
15 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
16 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
17 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
18 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
19 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
20 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
22 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
23 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
24 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
25 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
26 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
27 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
28 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
33 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
34 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
35 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
36 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
37 | github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57 h1:r+AdyYQnMjCqabCiXfAES7u0tbaqXlLXuZ5FT+5OEQs=
38 | github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
39 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
40 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
41 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
42 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
43 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
44 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
45 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
46 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
47 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
48 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
49 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
50 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
51 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
52 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
53 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
54 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
55 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
56 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
57 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
58 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
59 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
60 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
61 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
62 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
63 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
64 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
66 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
67 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
68 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
69 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
70 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
71 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
72 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
73 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
74 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
75 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
76 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
77 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
78 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
79 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
80 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
81 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
82 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
83 | github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d h1:r4iSf+UX1tNxFJZ64FsUoOfysT7TePSbRNz4/mYGUIE=
84 | github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
85 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
86 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
87 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
88 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
89 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
90 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
91 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
92 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
93 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
94 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
95 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
96 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
98 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
99 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
100 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
101 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
102 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
103 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
104 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
105 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
106 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
107 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
108 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
109 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
110 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
111 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
112 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
113 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
114 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
115 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
116 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
117 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
118 | github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5 h1:rW9pqjOLUVvikJFWrF53GlhmZNTFtsjjNA0LD2sYLvg=
119 | github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
120 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
121 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
122 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
123 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
124 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
125 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
126 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
127 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
128 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
129 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
130 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
131 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
132 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
133 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
134 | github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
135 | github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
136 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
137 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
138 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
139 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
140 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
141 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
142 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
143 | github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421 h1:s+WESDalIlUupImv6znWrHX6XIRXuVTBsX633p7Ymms=
144 | github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
145 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
146 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
147 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
148 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
149 | github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40 h1:2gwxRRQ5I+FcDbxGtkIC9kWD7EFBewHjQqD8rDQAVQA=
150 | github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
151 | github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07 h1:Bxzp40S+I62o0BB0Jd23i3OYDtIP2Gs8NtP+Sv6rYHg=
152 | github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07/go.mod h1:LLu5zwCkRPEBY0VPcRMqh58VtcO8Lp1DgqwstU7rYlk=
153 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
154 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
155 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
156 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
157 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
158 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
159 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
160 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
161 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
162 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
163 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
164 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
165 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
166 | github.com/subosito/gotenv v1.1.1 h1:TWxckSF6WVKWbo2M3tMqCtWa9NFUgqM1SSynxmYONOI=
167 | github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
168 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
169 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
170 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
171 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
172 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
173 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
174 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
175 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
176 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
177 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
178 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
179 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
180 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
181 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
182 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
183 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
184 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
185 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
186 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
187 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
188 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
189 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
190 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
191 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
192 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
193 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
194 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
195 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
196 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
197 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
198 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
199 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
200 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
201 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
202 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
203 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
204 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
205 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
206 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
207 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
208 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
209 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
210 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
211 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
212 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
213 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
214 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
215 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
216 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
217 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
218 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
219 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
220 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
221 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
222 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
223 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
224 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
225 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
226 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
227 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
228 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
229 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
230 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
231 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
232 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
233 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
234 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
235 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
236 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
237 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
238 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
239 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
240 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
241 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
242 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
243 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
244 |
--------------------------------------------------------------------------------
/helper/order_helper.go:
--------------------------------------------------------------------------------
1 | // Package helper 帮助函数
2 | package helper
3 |
4 | import (
5 | "fmt"
6 | "time"
7 | )
8 |
9 | // GetOrderIDTime 获取订单 ID
10 | func GetOrderIDTime() (orderID string) {
11 | currentTime := time.Now().Nanosecond()
12 | orderID = fmt.Sprintf("%d", currentTime)
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/helper/server_helper.go:
--------------------------------------------------------------------------------
1 | // Package helper 帮助函数
2 | package helper
3 |
4 | import (
5 | "net"
6 | )
7 |
8 | // GetServerIp 获取服务端 IP
9 | // 问题:我在本地多网卡机器上,运行分布式场景,此函数返回的ip有误导致rpc连接失败。 遂google结果如下:
10 | // 1、https://www.jianshu.com/p/301aabc06972
11 | // 2、https://www.cnblogs.com/chaselogs/p/11301940.html
12 | func GetServerIp() string {
13 | ip, err := externalIP()
14 | if err != nil {
15 | return ""
16 | }
17 | return ip.String()
18 | }
19 |
20 | func externalIP() (net.IP, error) {
21 | interfaces, err := net.Interfaces()
22 | if err != nil {
23 | return nil, err
24 | }
25 | for _, iface := range interfaces {
26 | if iface.Flags&net.FlagUp == 0 {
27 | continue // interface down
28 | }
29 | if iface.Flags&net.FlagLoopback != 0 {
30 | continue // loopback interface
31 | }
32 | addrs, err := iface.Addrs()
33 | if err != nil {
34 | return nil, err
35 | }
36 | for _, addr := range addrs {
37 | ip := getIpFromAddr(addr)
38 | if ip == nil {
39 | continue
40 | }
41 | return ip, nil
42 | }
43 | }
44 | return nil, err
45 | }
46 |
47 | func getIpFromAddr(addr net.Addr) net.IP {
48 | var ip net.IP
49 | switch v := addr.(type) {
50 | case *net.IPNet:
51 | ip = v.IP
52 | case *net.IPAddr:
53 | ip = v.IP
54 | }
55 | if ip == nil || ip.IsLoopback() {
56 | return nil
57 | }
58 | ip = ip.To4()
59 | if ip == nil {
60 | return nil // not an ipv4 address
61 | }
62 | return ip
63 | }
64 |
--------------------------------------------------------------------------------
/img/HTTP协议和WebSocket比较.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/HTTP协议和WebSocket比较.jpeg
--------------------------------------------------------------------------------
/img/HTTP协议和WebSocket比较.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/HTTP协议和WebSocket比较.png
--------------------------------------------------------------------------------
/img/jetbrains_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/jetbrains_logo.png
--------------------------------------------------------------------------------
/img/websocket接收和发送数据.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/websocket接收和发送数据.png
--------------------------------------------------------------------------------
/img/分布是系统随机给用户发送消息.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/分布是系统随机给用户发送消息.png
--------------------------------------------------------------------------------
/img/奥运五环.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/奥运五环.png
--------------------------------------------------------------------------------
/img/微信二维码.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/微信二维码.jpeg
--------------------------------------------------------------------------------
/img/服务端处理一个请求.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/服务端处理一个请求.jpeg
--------------------------------------------------------------------------------
/img/浏览器 Network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/浏览器 Network.png
--------------------------------------------------------------------------------
/img/浏览器开始支持webSocket的版本.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/浏览器开始支持webSocket的版本.jpeg
--------------------------------------------------------------------------------
/img/用户连接时序图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/用户连接时序图.png
--------------------------------------------------------------------------------
/img/网站架构图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/link1st/gowebsocket/cc6ce3de2bf10d9ae55f9359d79e3cc5f65065fb/img/网站架构图.png
--------------------------------------------------------------------------------
/lib/cache/server_cache.go:
--------------------------------------------------------------------------------
1 | // Package cache 缓存
2 | package cache
3 |
4 | import (
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "strconv"
9 |
10 | "github.com/link1st/gowebsocket/v2/lib/redislib"
11 | "github.com/link1st/gowebsocket/v2/models"
12 | )
13 |
14 | const (
15 | serversHashKey = "acc:hash:servers" // 全部的服务器
16 | serversHashCacheTime = 2 * 60 * 60 // key过期时间
17 | serversHashTimeout = 3 * 60 // 超时时间
18 | )
19 |
20 | func getServersHashKey() (key string) {
21 | key = fmt.Sprintf("%s", serversHashKey)
22 |
23 | return
24 | }
25 |
26 | // SetServerInfo 设置服务器信息
27 | func SetServerInfo(server *models.Server, currentTime uint64) (err error) {
28 | key := getServersHashKey()
29 | value := fmt.Sprintf("%d", currentTime)
30 | redisClient := redislib.GetClient()
31 | number, err := redisClient.Do(context.Background(), "hSet", key, server.String(), value).Int()
32 | if err != nil {
33 | fmt.Println("SetServerInfo", key, number, err)
34 | return
35 | }
36 | redisClient.Do(context.Background(), "Expire", key, serversHashCacheTime)
37 | return
38 | }
39 |
40 | // DelServerInfo 下线服务器信息
41 | func DelServerInfo(server *models.Server) (err error) {
42 | key := getServersHashKey()
43 | redisClient := redislib.GetClient()
44 | number, err := redisClient.Do(context.Background(), "hDel", key, server.String()).Int()
45 | if err != nil {
46 | fmt.Println("DelServerInfo", key, number, err)
47 | return
48 | }
49 | if number != 1 {
50 | return
51 | }
52 | redisClient.Do(context.Background(), "Expire", key, serversHashCacheTime)
53 | return
54 | }
55 |
56 | // GetServerAll 获取所有服务器
57 | func GetServerAll(currentTime uint64) (servers []*models.Server, err error) {
58 | servers = make([]*models.Server, 0)
59 | key := getServersHashKey()
60 | redisClient := redislib.GetClient()
61 | val, err := redisClient.Do(context.Background(), "hGetAll", key).Result()
62 | valByte, _ := json.Marshal(val)
63 | fmt.Println("GetServerAll", key, string(valByte))
64 | serverMap, err := redisClient.HGetAll(context.Background(), key).Result()
65 | if err != nil {
66 | fmt.Println("SetServerInfo", key, err)
67 | return
68 | }
69 | for key, value := range serverMap {
70 | valueUint64, err := strconv.ParseUint(value, 10, 64)
71 | if err != nil {
72 | fmt.Println("GetServerAll", key, err)
73 | return nil, err
74 | }
75 |
76 | // 超时
77 | if valueUint64+serversHashTimeout <= currentTime {
78 | continue
79 | }
80 | server, err := models.StringToServer(key)
81 | if err != nil {
82 | fmt.Println("GetServerAll", key, err)
83 | return nil, err
84 | }
85 | servers = append(servers, server)
86 | }
87 | return
88 | }
89 |
--------------------------------------------------------------------------------
/lib/cache/submit_cache.go:
--------------------------------------------------------------------------------
1 | // Package cache 缓存
2 | package cache
3 |
4 | import (
5 | "context"
6 | "fmt"
7 |
8 | "github.com/link1st/gowebsocket/v2/lib/redislib"
9 | )
10 |
11 | const (
12 | submitAgainPrefix = "acc:submit:again:" // 数据不重复提交
13 | )
14 |
15 | // getSubmitAgainKey 获取数据提交去除key
16 | func getSubmitAgainKey(from string, value string) (key string) {
17 | key = fmt.Sprintf("%s%s:%s", submitAgainPrefix, from, value)
18 |
19 | return
20 | }
21 |
22 | // submitAgain 重复提交
23 | // return true:重复提交 false:第一次提交
24 | func submitAgain(from string, second int, value string) (isSubmitAgain bool) {
25 |
26 | // 默认重复提交
27 | isSubmitAgain = true
28 | key := getSubmitAgainKey(from, value)
29 | redisClient := redislib.GetClient()
30 | number, err := redisClient.Do(context.Background(), "setNx", key, "1").Int()
31 | if err != nil {
32 | fmt.Println("submitAgain", key, number, err)
33 | return
34 | }
35 | if number != 1 {
36 | return
37 | }
38 |
39 | // 第一次提交
40 | isSubmitAgain = false
41 | redisClient.Do(context.Background(), "Expire", key, second)
42 | return
43 |
44 | }
45 |
46 | // SeqDuplicates Seq 重复提交
47 | func SeqDuplicates(seq string) (result bool) {
48 | result = submitAgain("seq", 12*60*60, seq)
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/lib/cache/user_cache.go:
--------------------------------------------------------------------------------
1 | // Package cache 缓存
2 | package cache
3 |
4 | import (
5 | "context"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 |
10 | "github.com/link1st/gowebsocket/v2/lib/redislib"
11 | "github.com/link1st/gowebsocket/v2/models"
12 |
13 | "github.com/redis/go-redis/v9"
14 | )
15 |
16 | const (
17 | userOnlinePrefix = "acc:user:online:" // 用户在线状态
18 | userOnlineCacheTime = 24 * 60 * 60
19 | )
20 |
21 | func getUserOnlineKey(userKey string) (key string) {
22 | key = fmt.Sprintf("%s%s", userOnlinePrefix, userKey)
23 | return
24 | }
25 |
26 | // GetUserOnlineInfo 获取用户在线信息
27 | func GetUserOnlineInfo(userKey string) (userOnline *models.UserOnline, err error) {
28 | redisClient := redislib.GetClient()
29 | key := getUserOnlineKey(userKey)
30 | data, err := redisClient.Get(context.Background(), key).Bytes()
31 | if err != nil {
32 | if errors.Is(err, redis.Nil) {
33 | fmt.Println("GetUserOnlineInfo", userKey, err)
34 | return
35 | }
36 | fmt.Println("GetUserOnlineInfo", userKey, err)
37 | return
38 | }
39 | userOnline = &models.UserOnline{}
40 | err = json.Unmarshal(data, userOnline)
41 | if err != nil {
42 | fmt.Println("获取用户在线数据 json Unmarshal", userKey, err)
43 | return
44 | }
45 | fmt.Println("获取用户在线数据", userKey, "time", userOnline.LoginTime, userOnline.HeartbeatTime, "AccIp",
46 | userOnline.AccIp, userOnline.IsLogoff)
47 | return
48 | }
49 |
50 | // SetUserOnlineInfo 设置用户在线数据
51 | func SetUserOnlineInfo(userKey string, userOnline *models.UserOnline) (err error) {
52 | redisClient := redislib.GetClient()
53 | key := getUserOnlineKey(userKey)
54 | valueByte, err := json.Marshal(userOnline)
55 | if err != nil {
56 | fmt.Println("设置用户在线数据 json Marshal", key, err)
57 | return
58 | }
59 | _, err = redisClient.Do(context.Background(), "setEx", key, userOnlineCacheTime, string(valueByte)).Result()
60 | if err != nil {
61 | fmt.Println("设置用户在线数据 ", key, err)
62 | return
63 | }
64 | return
65 | }
66 |
--------------------------------------------------------------------------------
/lib/redislib/redis_lib.go:
--------------------------------------------------------------------------------
1 | // Package redislib redis 库
2 | package redislib
3 |
4 | import (
5 | "context"
6 | "fmt"
7 |
8 | "github.com/redis/go-redis/v9"
9 | "github.com/spf13/viper"
10 | )
11 |
12 | var (
13 | client *redis.Client
14 | )
15 |
16 | // NewClient 初始化 Redis 客户端
17 | func NewClient() {
18 | client = redis.NewClient(&redis.Options{
19 | Addr: viper.GetString("redis.addr"),
20 | Password: viper.GetString("redis.password"),
21 | DB: viper.GetInt("redis.DB"),
22 | PoolSize: viper.GetInt("redis.poolSize"),
23 | MinIdleConns: viper.GetInt("redis.minIdleConns"),
24 | })
25 | pong, err := client.Ping(context.Background()).Result()
26 | fmt.Println("初始化redis:", pong, err)
27 | }
28 |
29 | // GetClient 获取客户端
30 | func GetClient() (c *redis.Client) {
31 | return client
32 | }
33 |
--------------------------------------------------------------------------------
/log/.gitignore:
--------------------------------------------------------------------------------
1 | !.gitignore
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Package main 实现一个基于websocket分布式聊天(IM)系统。
2 | package main
3 |
4 | import (
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "os"
9 | "os/exec"
10 | "time"
11 |
12 | "github.com/gin-gonic/gin"
13 | "github.com/spf13/viper"
14 |
15 | "github.com/link1st/gowebsocket/v2/lib/redislib"
16 | "github.com/link1st/gowebsocket/v2/routers"
17 | "github.com/link1st/gowebsocket/v2/servers/grpcserver"
18 | "github.com/link1st/gowebsocket/v2/servers/task"
19 | "github.com/link1st/gowebsocket/v2/servers/websocket"
20 | )
21 |
22 | func main() {
23 | initConfig()
24 | initFile()
25 | initRedis()
26 | router := gin.Default()
27 |
28 | // 初始化路由
29 | routers.Init(router)
30 | routers.WebsocketInit()
31 |
32 | // 定时任务
33 | task.Init()
34 |
35 | // 服务注册
36 | task.ServerInit()
37 | go websocket.StartWebSocket()
38 |
39 | // grpc
40 | go grpcserver.Init()
41 | go open()
42 | httpPort := viper.GetString("app.httpPort")
43 | _ = http.ListenAndServe(":"+httpPort, router)
44 | }
45 |
46 | // initFile 初始化日志
47 | func initFile() {
48 | // Disable Console Color, you don't need console color when writing the logs to file.
49 | gin.DisableConsoleColor()
50 |
51 | // Logging to a file.
52 | logFile := viper.GetString("app.logFile")
53 | f, _ := os.Create(logFile)
54 | gin.DefaultWriter = io.MultiWriter(f)
55 | }
56 |
57 | func initConfig() {
58 | viper.SetConfigName("config/app")
59 | viper.AddConfigPath(".")
60 | err := viper.ReadInConfig()
61 | if err != nil {
62 | panic(fmt.Errorf("Fatal error config file: %s \n", err))
63 | }
64 | fmt.Println("config app:", viper.Get("app"))
65 | fmt.Println("config redis:", viper.Get("redis"))
66 |
67 | }
68 |
69 | func initRedis() {
70 | redislib.NewClient()
71 | }
72 |
73 | func open() {
74 | time.Sleep(1000 * time.Millisecond)
75 | httpUrl := viper.GetString("app.httpUrl")
76 | httpUrl = "http://" + httpUrl + "/home/index"
77 | fmt.Println("访问页面体验:", httpUrl)
78 | cmd := exec.Command("open", httpUrl)
79 | _, _ = cmd.Output()
80 | }
81 |
--------------------------------------------------------------------------------
/models/msg_model.go:
--------------------------------------------------------------------------------
1 | // Package models 数据模型
2 | package models
3 |
4 | import "github.com/link1st/gowebsocket/v2/common"
5 |
6 | const (
7 | // MessageTypeText 文本类型消息
8 | MessageTypeText = "text"
9 | // MessageCmdMsg 文本类型消息
10 | MessageCmdMsg = "msg"
11 | // MessageCmdEnter 用户进入类型消息
12 | MessageCmdEnter = "enter"
13 | // MessageCmdExit 用户退出类型消息
14 | MessageCmdExit = "exit"
15 | )
16 |
17 | // Message 消息的定义
18 | type Message struct {
19 | Target string `json:"target"` // 目标
20 | Type string `json:"type"` // 消息类型 text/img/
21 | Msg string `json:"msg"` // 消息内容
22 | From string `json:"from"` // 发送者
23 | }
24 |
25 | // NewMsg 创建新的消息
26 | func NewMsg(from string, Msg string) (message *Message) {
27 | message = &Message{
28 | Type: MessageTypeText,
29 | From: from,
30 | Msg: Msg,
31 | }
32 | return
33 | }
34 |
35 | func getTextMsgData(cmd, uuID, msgID, message string) string {
36 | textMsg := NewMsg(uuID, message)
37 | head := NewResponseHead(msgID, cmd, common.OK, "Ok", textMsg)
38 |
39 | return head.String()
40 | }
41 |
42 | // GetMsgData 文本消息
43 | func GetMsgData(uuID, msgID, cmd, message string) string {
44 | return getTextMsgData(cmd, uuID, msgID, message)
45 | }
46 |
47 | // GetTextMsgData 文本消息
48 | func GetTextMsgData(uuID, msgID, message string) string {
49 | return getTextMsgData("msg", uuID, msgID, message)
50 | }
51 |
52 | // GetTextMsgDataEnter 用户进入消息
53 | func GetTextMsgDataEnter(uuID, msgID, message string) string {
54 | return getTextMsgData("enter", uuID, msgID, message)
55 | }
56 |
57 | // GetTextMsgDataExit 用户退出消息
58 | func GetTextMsgDataExit(uuID, msgID, message string) string {
59 | return getTextMsgData("exit", uuID, msgID, message)
60 | }
61 |
--------------------------------------------------------------------------------
/models/request_model.go:
--------------------------------------------------------------------------------
1 | // Package models 数据模型
2 | package models
3 |
4 | // Request 通用请求数据格式
5 | type Request struct {
6 | Seq string `json:"seq"` // 消息的唯一ID
7 | Cmd string `json:"cmd"` // 请求命令字
8 | Data interface{} `json:"data,omitempty"` // 数据 json
9 | }
10 |
11 | // Login 登录请求数据
12 | type Login struct {
13 | ServiceToken string `json:"serviceToken"` // 验证用户是否登录
14 | AppID uint32 `json:"appID,omitempty"`
15 | UserID string `json:"userID,omitempty"`
16 | }
17 |
18 | // HeartBeat 心跳请求数据
19 | type HeartBeat struct {
20 | UserID string `json:"userID,omitempty"`
21 | }
22 |
--------------------------------------------------------------------------------
/models/response_model.go:
--------------------------------------------------------------------------------
1 | // Package models 数据模型
2 | package models
3 |
4 | import "encoding/json"
5 |
6 | // Head 响应数据头
7 | type Head struct {
8 | Seq string `json:"seq"` // 消息的ID
9 | Cmd string `json:"cmd"` // 消息的cmd 动作
10 | Response *Response `json:"response"` // 消息体
11 | }
12 |
13 | // Response 响应数据体
14 | type Response struct {
15 | Code uint32 `json:"code"`
16 | CodeMsg string `json:"codeMsg"`
17 | Data interface{} `json:"data"` // 数据 json
18 | }
19 |
20 | // PushMsg 数据结构体
21 | type PushMsg struct {
22 | Seq string `json:"seq"`
23 | Uuid uint64 `json:"uuid"`
24 | Type string `json:"type"`
25 | Msg string `json:"msg"`
26 | }
27 |
28 | // NewResponseHead 设置返回消息
29 | func NewResponseHead(seq string, cmd string, code uint32, codeMsg string, data interface{}) *Head {
30 | response := NewResponse(code, codeMsg, data)
31 |
32 | return &Head{Seq: seq, Cmd: cmd, Response: response}
33 | }
34 |
35 | // String to string
36 | func (h *Head) String() (headStr string) {
37 | headBytes, _ := json.Marshal(h)
38 | headStr = string(headBytes)
39 |
40 | return
41 | }
42 |
43 | // NewResponse 创建新的响应
44 | func NewResponse(code uint32, codeMsg string, data interface{}) *Response {
45 | return &Response{Code: code, CodeMsg: codeMsg, Data: data}
46 | }
47 |
--------------------------------------------------------------------------------
/models/server_model.go:
--------------------------------------------------------------------------------
1 | // Package models 数据模型
2 | package models
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | "strings"
8 | )
9 |
10 | // Server 服务器结构体
11 | type Server struct {
12 | Ip string `json:"ip"` // ip
13 | Port string `json:"port"` // 端口
14 | }
15 |
16 | // NewServer 创建
17 | func NewServer(ip string, port string) *Server {
18 | return &Server{Ip: ip, Port: port}
19 | }
20 |
21 | // String to string
22 | func (s *Server) String() (str string) {
23 | if s == nil {
24 | return
25 | }
26 | str = fmt.Sprintf("%s:%s", s.Ip, s.Port)
27 | return
28 | }
29 |
30 | // StringToServer 字符串转结构体
31 | func StringToServer(str string) (server *Server, err error) {
32 | list := strings.Split(str, ":")
33 | if len(list) != 2 {
34 | return nil, errors.New("err")
35 | }
36 | server = &Server{
37 | Ip: list[0],
38 | Port: list[1],
39 | }
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/models/user_model.go:
--------------------------------------------------------------------------------
1 | // Package models 数据模型
2 | package models
3 |
4 | import (
5 | "fmt"
6 | "time"
7 | )
8 |
9 | const (
10 | heartbeatTimeout = 3 * 60 // 用户心跳超时时间
11 | )
12 |
13 | // UserOnline 用户在线状态
14 | type UserOnline struct {
15 | AccIp string `json:"accIp"` // acc Ip
16 | AccPort string `json:"accPort"` // acc 端口
17 | AppID uint32 `json:"appID"` // appID
18 | UserID string `json:"userID"` // 用户ID
19 | ClientIp string `json:"clientIp"` // 客户端Ip
20 | ClientPort string `json:"clientPort"` // 客户端端口
21 | LoginTime uint64 `json:"loginTime"` // 用户上次登录时间
22 | HeartbeatTime uint64 `json:"heartbeatTime"` // 用户上次心跳时间
23 | LogOutTime uint64 `json:"logOutTime"` // 用户退出登录的时间
24 | Qua string `json:"qua"` // qua
25 | DeviceInfo string `json:"deviceInfo"` // 设备信息
26 | IsLogoff bool `json:"isLogoff"` // 是否下线
27 | }
28 |
29 | // UserLogin 用户登录
30 | func UserLogin(accIp, accPort string, appID uint32, userID string, addr string,
31 | loginTime uint64) (userOnline *UserOnline) {
32 | userOnline = &UserOnline{
33 | AccIp: accIp,
34 | AccPort: accPort,
35 | AppID: appID,
36 | UserID: userID,
37 | ClientIp: addr,
38 | LoginTime: loginTime,
39 | HeartbeatTime: loginTime,
40 | IsLogoff: false,
41 | }
42 | return
43 | }
44 |
45 | // Heartbeat 用户心跳
46 | func (u *UserOnline) Heartbeat(currentTime uint64) {
47 | u.HeartbeatTime = currentTime
48 | u.IsLogoff = false
49 | return
50 | }
51 |
52 | // LogOut 用户退出登录
53 | func (u *UserOnline) LogOut() {
54 | currentTime := uint64(time.Now().Unix())
55 | u.LogOutTime = currentTime
56 | u.IsLogoff = true
57 | return
58 | }
59 |
60 | // IsOnline 用户是否在线
61 | func (u *UserOnline) IsOnline() (online bool) {
62 | if u.IsLogoff {
63 | return
64 | }
65 | currentTime := uint64(time.Now().Unix())
66 | if u.HeartbeatTime < (currentTime - heartbeatTimeout) {
67 | fmt.Println("用户是否在线 心跳超时", u.AppID, u.UserID, u.HeartbeatTime)
68 | return
69 | }
70 | if u.IsLogoff {
71 | fmt.Println("用户是否在线 用户已经下线", u.AppID, u.UserID)
72 | return
73 | }
74 | return true
75 | }
76 |
77 | // UserIsLocal 用户是否在本台机器上
78 | func (u *UserOnline) UserIsLocal(localIp, localPort string) (result bool) {
79 | if u.AccIp == localIp && u.AccPort == localPort {
80 | result = true
81 | return
82 | }
83 | return
84 | }
85 |
--------------------------------------------------------------------------------
/protobuf/gen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | protoc --go_out=. --go_opt=paths=source_relative \
4 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \
5 | im_protobuf.proto
6 |
--------------------------------------------------------------------------------
/protobuf/im_protobuf.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.31.0
4 | // protoc v4.24.3
5 | // source: im_protobuf.proto
6 |
7 | package protobuf
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | // 查询用户是否在线
24 | type QueryUsersOnlineReq struct {
25 | state protoimpl.MessageState
26 | sizeCache protoimpl.SizeCache
27 | unknownFields protoimpl.UnknownFields
28 |
29 | AppID uint32 `protobuf:"varint,1,opt,name=appID,proto3" json:"appID,omitempty"` // AppID
30 | UserID string `protobuf:"bytes,2,opt,name=userID,proto3" json:"userID,omitempty"` // 用户ID
31 | }
32 |
33 | func (x *QueryUsersOnlineReq) Reset() {
34 | *x = QueryUsersOnlineReq{}
35 | if protoimpl.UnsafeEnabled {
36 | mi := &file_im_protobuf_proto_msgTypes[0]
37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
38 | ms.StoreMessageInfo(mi)
39 | }
40 | }
41 |
42 | func (x *QueryUsersOnlineReq) String() string {
43 | return protoimpl.X.MessageStringOf(x)
44 | }
45 |
46 | func (*QueryUsersOnlineReq) ProtoMessage() {}
47 |
48 | func (x *QueryUsersOnlineReq) ProtoReflect() protoreflect.Message {
49 | mi := &file_im_protobuf_proto_msgTypes[0]
50 | if protoimpl.UnsafeEnabled && x != nil {
51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
52 | if ms.LoadMessageInfo() == nil {
53 | ms.StoreMessageInfo(mi)
54 | }
55 | return ms
56 | }
57 | return mi.MessageOf(x)
58 | }
59 |
60 | // Deprecated: Use QueryUsersOnlineReq.ProtoReflect.Descriptor instead.
61 | func (*QueryUsersOnlineReq) Descriptor() ([]byte, []int) {
62 | return file_im_protobuf_proto_rawDescGZIP(), []int{0}
63 | }
64 |
65 | func (x *QueryUsersOnlineReq) GetAppID() uint32 {
66 | if x != nil {
67 | return x.AppID
68 | }
69 | return 0
70 | }
71 |
72 | func (x *QueryUsersOnlineReq) GetUserID() string {
73 | if x != nil {
74 | return x.UserID
75 | }
76 | return ""
77 | }
78 |
79 | type QueryUsersOnlineRsp struct {
80 | state protoimpl.MessageState
81 | sizeCache protoimpl.SizeCache
82 | unknownFields protoimpl.UnknownFields
83 |
84 | RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
85 | ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
86 | Online bool `protobuf:"varint,3,opt,name=online,proto3" json:"online,omitempty"`
87 | }
88 |
89 | func (x *QueryUsersOnlineRsp) Reset() {
90 | *x = QueryUsersOnlineRsp{}
91 | if protoimpl.UnsafeEnabled {
92 | mi := &file_im_protobuf_proto_msgTypes[1]
93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
94 | ms.StoreMessageInfo(mi)
95 | }
96 | }
97 |
98 | func (x *QueryUsersOnlineRsp) String() string {
99 | return protoimpl.X.MessageStringOf(x)
100 | }
101 |
102 | func (*QueryUsersOnlineRsp) ProtoMessage() {}
103 |
104 | func (x *QueryUsersOnlineRsp) ProtoReflect() protoreflect.Message {
105 | mi := &file_im_protobuf_proto_msgTypes[1]
106 | if protoimpl.UnsafeEnabled && x != nil {
107 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
108 | if ms.LoadMessageInfo() == nil {
109 | ms.StoreMessageInfo(mi)
110 | }
111 | return ms
112 | }
113 | return mi.MessageOf(x)
114 | }
115 |
116 | // Deprecated: Use QueryUsersOnlineRsp.ProtoReflect.Descriptor instead.
117 | func (*QueryUsersOnlineRsp) Descriptor() ([]byte, []int) {
118 | return file_im_protobuf_proto_rawDescGZIP(), []int{1}
119 | }
120 |
121 | func (x *QueryUsersOnlineRsp) GetRetCode() uint32 {
122 | if x != nil {
123 | return x.RetCode
124 | }
125 | return 0
126 | }
127 |
128 | func (x *QueryUsersOnlineRsp) GetErrMsg() string {
129 | if x != nil {
130 | return x.ErrMsg
131 | }
132 | return ""
133 | }
134 |
135 | func (x *QueryUsersOnlineRsp) GetOnline() bool {
136 | if x != nil {
137 | return x.Online
138 | }
139 | return false
140 | }
141 |
142 | // 发送消息
143 | type SendMsgReq struct {
144 | state protoimpl.MessageState
145 | sizeCache protoimpl.SizeCache
146 | unknownFields protoimpl.UnknownFields
147 |
148 | Seq string `protobuf:"bytes,1,opt,name=seq,proto3" json:"seq,omitempty"` // 序列号
149 | AppID uint32 `protobuf:"varint,2,opt,name=appID,proto3" json:"appID,omitempty"` // appID/房间ID
150 | UserID string `protobuf:"bytes,3,opt,name=userID,proto3" json:"userID,omitempty"` // 用户ID
151 | Cms string `protobuf:"bytes,4,opt,name=cms,proto3" json:"cms,omitempty"` // cms 动作: msg/enter/exit
152 | Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` // type 消息类型,默认是 text
153 | Msg string `protobuf:"bytes,6,opt,name=msg,proto3" json:"msg,omitempty"` // msg
154 | IsLocal bool `protobuf:"varint,7,opt,name=isLocal,proto3" json:"isLocal,omitempty"` // 是否查询本机 acc内部调用为:true(本机查询不到即结束)
155 | }
156 |
157 | func (x *SendMsgReq) Reset() {
158 | *x = SendMsgReq{}
159 | if protoimpl.UnsafeEnabled {
160 | mi := &file_im_protobuf_proto_msgTypes[2]
161 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
162 | ms.StoreMessageInfo(mi)
163 | }
164 | }
165 |
166 | func (x *SendMsgReq) String() string {
167 | return protoimpl.X.MessageStringOf(x)
168 | }
169 |
170 | func (*SendMsgReq) ProtoMessage() {}
171 |
172 | func (x *SendMsgReq) ProtoReflect() protoreflect.Message {
173 | mi := &file_im_protobuf_proto_msgTypes[2]
174 | if protoimpl.UnsafeEnabled && x != nil {
175 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
176 | if ms.LoadMessageInfo() == nil {
177 | ms.StoreMessageInfo(mi)
178 | }
179 | return ms
180 | }
181 | return mi.MessageOf(x)
182 | }
183 |
184 | // Deprecated: Use SendMsgReq.ProtoReflect.Descriptor instead.
185 | func (*SendMsgReq) Descriptor() ([]byte, []int) {
186 | return file_im_protobuf_proto_rawDescGZIP(), []int{2}
187 | }
188 |
189 | func (x *SendMsgReq) GetSeq() string {
190 | if x != nil {
191 | return x.Seq
192 | }
193 | return ""
194 | }
195 |
196 | func (x *SendMsgReq) GetAppID() uint32 {
197 | if x != nil {
198 | return x.AppID
199 | }
200 | return 0
201 | }
202 |
203 | func (x *SendMsgReq) GetUserID() string {
204 | if x != nil {
205 | return x.UserID
206 | }
207 | return ""
208 | }
209 |
210 | func (x *SendMsgReq) GetCms() string {
211 | if x != nil {
212 | return x.Cms
213 | }
214 | return ""
215 | }
216 |
217 | func (x *SendMsgReq) GetType() string {
218 | if x != nil {
219 | return x.Type
220 | }
221 | return ""
222 | }
223 |
224 | func (x *SendMsgReq) GetMsg() string {
225 | if x != nil {
226 | return x.Msg
227 | }
228 | return ""
229 | }
230 |
231 | func (x *SendMsgReq) GetIsLocal() bool {
232 | if x != nil {
233 | return x.IsLocal
234 | }
235 | return false
236 | }
237 |
238 | type SendMsgRsp struct {
239 | state protoimpl.MessageState
240 | sizeCache protoimpl.SizeCache
241 | unknownFields protoimpl.UnknownFields
242 |
243 | RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
244 | ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
245 | SendMsgID string `protobuf:"bytes,3,opt,name=sendMsgID,proto3" json:"sendMsgID,omitempty"`
246 | }
247 |
248 | func (x *SendMsgRsp) Reset() {
249 | *x = SendMsgRsp{}
250 | if protoimpl.UnsafeEnabled {
251 | mi := &file_im_protobuf_proto_msgTypes[3]
252 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
253 | ms.StoreMessageInfo(mi)
254 | }
255 | }
256 |
257 | func (x *SendMsgRsp) String() string {
258 | return protoimpl.X.MessageStringOf(x)
259 | }
260 |
261 | func (*SendMsgRsp) ProtoMessage() {}
262 |
263 | func (x *SendMsgRsp) ProtoReflect() protoreflect.Message {
264 | mi := &file_im_protobuf_proto_msgTypes[3]
265 | if protoimpl.UnsafeEnabled && x != nil {
266 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
267 | if ms.LoadMessageInfo() == nil {
268 | ms.StoreMessageInfo(mi)
269 | }
270 | return ms
271 | }
272 | return mi.MessageOf(x)
273 | }
274 |
275 | // Deprecated: Use SendMsgRsp.ProtoReflect.Descriptor instead.
276 | func (*SendMsgRsp) Descriptor() ([]byte, []int) {
277 | return file_im_protobuf_proto_rawDescGZIP(), []int{3}
278 | }
279 |
280 | func (x *SendMsgRsp) GetRetCode() uint32 {
281 | if x != nil {
282 | return x.RetCode
283 | }
284 | return 0
285 | }
286 |
287 | func (x *SendMsgRsp) GetErrMsg() string {
288 | if x != nil {
289 | return x.ErrMsg
290 | }
291 | return ""
292 | }
293 |
294 | func (x *SendMsgRsp) GetSendMsgID() string {
295 | if x != nil {
296 | return x.SendMsgID
297 | }
298 | return ""
299 | }
300 |
301 | // 给这台机器的房间内所有用户发送消息
302 | type SendMsgAllReq struct {
303 | state protoimpl.MessageState
304 | sizeCache protoimpl.SizeCache
305 | unknownFields protoimpl.UnknownFields
306 |
307 | Seq string `protobuf:"bytes,1,opt,name=seq,proto3" json:"seq,omitempty"` // 序列号
308 | AppID uint32 `protobuf:"varint,2,opt,name=appID,proto3" json:"appID,omitempty"` // appID/房间ID
309 | UserID string `protobuf:"bytes,3,opt,name=userID,proto3" json:"userID,omitempty"` // 不发送的用户ID
310 | Cms string `protobuf:"bytes,4,opt,name=cms,proto3" json:"cms,omitempty"` // cms 动作: msg/enter/exit
311 | Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` // type 消息类型,默认是 text
312 | Msg string `protobuf:"bytes,6,opt,name=msg,proto3" json:"msg,omitempty"` // msg
313 | }
314 |
315 | func (x *SendMsgAllReq) Reset() {
316 | *x = SendMsgAllReq{}
317 | if protoimpl.UnsafeEnabled {
318 | mi := &file_im_protobuf_proto_msgTypes[4]
319 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
320 | ms.StoreMessageInfo(mi)
321 | }
322 | }
323 |
324 | func (x *SendMsgAllReq) String() string {
325 | return protoimpl.X.MessageStringOf(x)
326 | }
327 |
328 | func (*SendMsgAllReq) ProtoMessage() {}
329 |
330 | func (x *SendMsgAllReq) ProtoReflect() protoreflect.Message {
331 | mi := &file_im_protobuf_proto_msgTypes[4]
332 | if protoimpl.UnsafeEnabled && x != nil {
333 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
334 | if ms.LoadMessageInfo() == nil {
335 | ms.StoreMessageInfo(mi)
336 | }
337 | return ms
338 | }
339 | return mi.MessageOf(x)
340 | }
341 |
342 | // Deprecated: Use SendMsgAllReq.ProtoReflect.Descriptor instead.
343 | func (*SendMsgAllReq) Descriptor() ([]byte, []int) {
344 | return file_im_protobuf_proto_rawDescGZIP(), []int{4}
345 | }
346 |
347 | func (x *SendMsgAllReq) GetSeq() string {
348 | if x != nil {
349 | return x.Seq
350 | }
351 | return ""
352 | }
353 |
354 | func (x *SendMsgAllReq) GetAppID() uint32 {
355 | if x != nil {
356 | return x.AppID
357 | }
358 | return 0
359 | }
360 |
361 | func (x *SendMsgAllReq) GetUserID() string {
362 | if x != nil {
363 | return x.UserID
364 | }
365 | return ""
366 | }
367 |
368 | func (x *SendMsgAllReq) GetCms() string {
369 | if x != nil {
370 | return x.Cms
371 | }
372 | return ""
373 | }
374 |
375 | func (x *SendMsgAllReq) GetType() string {
376 | if x != nil {
377 | return x.Type
378 | }
379 | return ""
380 | }
381 |
382 | func (x *SendMsgAllReq) GetMsg() string {
383 | if x != nil {
384 | return x.Msg
385 | }
386 | return ""
387 | }
388 |
389 | type SendMsgAllRsp struct {
390 | state protoimpl.MessageState
391 | sizeCache protoimpl.SizeCache
392 | unknownFields protoimpl.UnknownFields
393 |
394 | RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
395 | ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
396 | SendMsgID string `protobuf:"bytes,3,opt,name=sendMsgID,proto3" json:"sendMsgID,omitempty"`
397 | }
398 |
399 | func (x *SendMsgAllRsp) Reset() {
400 | *x = SendMsgAllRsp{}
401 | if protoimpl.UnsafeEnabled {
402 | mi := &file_im_protobuf_proto_msgTypes[5]
403 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
404 | ms.StoreMessageInfo(mi)
405 | }
406 | }
407 |
408 | func (x *SendMsgAllRsp) String() string {
409 | return protoimpl.X.MessageStringOf(x)
410 | }
411 |
412 | func (*SendMsgAllRsp) ProtoMessage() {}
413 |
414 | func (x *SendMsgAllRsp) ProtoReflect() protoreflect.Message {
415 | mi := &file_im_protobuf_proto_msgTypes[5]
416 | if protoimpl.UnsafeEnabled && x != nil {
417 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
418 | if ms.LoadMessageInfo() == nil {
419 | ms.StoreMessageInfo(mi)
420 | }
421 | return ms
422 | }
423 | return mi.MessageOf(x)
424 | }
425 |
426 | // Deprecated: Use SendMsgAllRsp.ProtoReflect.Descriptor instead.
427 | func (*SendMsgAllRsp) Descriptor() ([]byte, []int) {
428 | return file_im_protobuf_proto_rawDescGZIP(), []int{5}
429 | }
430 |
431 | func (x *SendMsgAllRsp) GetRetCode() uint32 {
432 | if x != nil {
433 | return x.RetCode
434 | }
435 | return 0
436 | }
437 |
438 | func (x *SendMsgAllRsp) GetErrMsg() string {
439 | if x != nil {
440 | return x.ErrMsg
441 | }
442 | return ""
443 | }
444 |
445 | func (x *SendMsgAllRsp) GetSendMsgID() string {
446 | if x != nil {
447 | return x.SendMsgID
448 | }
449 | return ""
450 | }
451 |
452 | // 获取用户列表
453 | type GetUserListReq struct {
454 | state protoimpl.MessageState
455 | sizeCache protoimpl.SizeCache
456 | unknownFields protoimpl.UnknownFields
457 |
458 | AppID uint32 `protobuf:"varint,1,opt,name=appID,proto3" json:"appID,omitempty"`
459 | }
460 |
461 | func (x *GetUserListReq) Reset() {
462 | *x = GetUserListReq{}
463 | if protoimpl.UnsafeEnabled {
464 | mi := &file_im_protobuf_proto_msgTypes[6]
465 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
466 | ms.StoreMessageInfo(mi)
467 | }
468 | }
469 |
470 | func (x *GetUserListReq) String() string {
471 | return protoimpl.X.MessageStringOf(x)
472 | }
473 |
474 | func (*GetUserListReq) ProtoMessage() {}
475 |
476 | func (x *GetUserListReq) ProtoReflect() protoreflect.Message {
477 | mi := &file_im_protobuf_proto_msgTypes[6]
478 | if protoimpl.UnsafeEnabled && x != nil {
479 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
480 | if ms.LoadMessageInfo() == nil {
481 | ms.StoreMessageInfo(mi)
482 | }
483 | return ms
484 | }
485 | return mi.MessageOf(x)
486 | }
487 |
488 | // Deprecated: Use GetUserListReq.ProtoReflect.Descriptor instead.
489 | func (*GetUserListReq) Descriptor() ([]byte, []int) {
490 | return file_im_protobuf_proto_rawDescGZIP(), []int{6}
491 | }
492 |
493 | func (x *GetUserListReq) GetAppID() uint32 {
494 | if x != nil {
495 | return x.AppID
496 | }
497 | return 0
498 | }
499 |
500 | type GetUserListRsp struct {
501 | state protoimpl.MessageState
502 | sizeCache protoimpl.SizeCache
503 | unknownFields protoimpl.UnknownFields
504 |
505 | RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
506 | ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
507 | UserID []string `protobuf:"bytes,3,rep,name=userID,proto3" json:"userID,omitempty"`
508 | }
509 |
510 | func (x *GetUserListRsp) Reset() {
511 | *x = GetUserListRsp{}
512 | if protoimpl.UnsafeEnabled {
513 | mi := &file_im_protobuf_proto_msgTypes[7]
514 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
515 | ms.StoreMessageInfo(mi)
516 | }
517 | }
518 |
519 | func (x *GetUserListRsp) String() string {
520 | return protoimpl.X.MessageStringOf(x)
521 | }
522 |
523 | func (*GetUserListRsp) ProtoMessage() {}
524 |
525 | func (x *GetUserListRsp) ProtoReflect() protoreflect.Message {
526 | mi := &file_im_protobuf_proto_msgTypes[7]
527 | if protoimpl.UnsafeEnabled && x != nil {
528 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
529 | if ms.LoadMessageInfo() == nil {
530 | ms.StoreMessageInfo(mi)
531 | }
532 | return ms
533 | }
534 | return mi.MessageOf(x)
535 | }
536 |
537 | // Deprecated: Use GetUserListRsp.ProtoReflect.Descriptor instead.
538 | func (*GetUserListRsp) Descriptor() ([]byte, []int) {
539 | return file_im_protobuf_proto_rawDescGZIP(), []int{7}
540 | }
541 |
542 | func (x *GetUserListRsp) GetRetCode() uint32 {
543 | if x != nil {
544 | return x.RetCode
545 | }
546 | return 0
547 | }
548 |
549 | func (x *GetUserListRsp) GetErrMsg() string {
550 | if x != nil {
551 | return x.ErrMsg
552 | }
553 | return ""
554 | }
555 |
556 | func (x *GetUserListRsp) GetUserID() []string {
557 | if x != nil {
558 | return x.UserID
559 | }
560 | return nil
561 | }
562 |
563 | var File_im_protobuf_proto protoreflect.FileDescriptor
564 |
565 | var file_im_protobuf_proto_rawDesc = []byte{
566 | 0x0a, 0x11, 0x69, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x70, 0x72,
567 | 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x43, 0x0a,
568 | 0x13, 0x51, 0x75, 0x65, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e,
569 | 0x65, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x18, 0x01, 0x20,
570 | 0x01, 0x28, 0x0d, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73,
571 | 0x65, 0x72, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
572 | 0x49, 0x44, 0x22, 0x5f, 0x0a, 0x13, 0x51, 0x75, 0x65, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x73,
573 | 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x52, 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x74,
574 | 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x72, 0x65, 0x74, 0x43,
575 | 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x18, 0x02, 0x20,
576 | 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x6f,
577 | 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6f, 0x6e, 0x6c,
578 | 0x69, 0x6e, 0x65, 0x22, 0x9e, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x52,
579 | 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
580 | 0x03, 0x73, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x18, 0x02, 0x20,
581 | 0x01, 0x28, 0x0d, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73,
582 | 0x65, 0x72, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
583 | 0x49, 0x44, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
584 | 0x03, 0x63, 0x6d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01,
585 | 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18,
586 | 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x73,
587 | 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x4c,
588 | 0x6f, 0x63, 0x61, 0x6c, 0x22, 0x5c, 0x0a, 0x0a, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x52,
589 | 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
590 | 0x01, 0x28, 0x0d, 0x52, 0x07, 0x72, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06,
591 | 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72,
592 | 0x72, 0x4d, 0x73, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x49,
593 | 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67,
594 | 0x49, 0x44, 0x22, 0x87, 0x01, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x41, 0x6c,
595 | 0x6c, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28,
596 | 0x09, 0x52, 0x03, 0x73, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x18,
597 | 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06,
598 | 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73,
599 | 0x65, 0x72, 0x49, 0x44, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
600 | 0x09, 0x52, 0x03, 0x63, 0x6d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05,
601 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73,
602 | 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x5f, 0x0a, 0x0d,
603 | 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x41, 0x6c, 0x6c, 0x52, 0x73, 0x70, 0x12, 0x18, 0x0a,
604 | 0x07, 0x72, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07,
605 | 0x72, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73,
606 | 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x12,
607 | 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01,
608 | 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x49, 0x44, 0x22, 0x26, 0x0a,
609 | 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x12,
610 | 0x14, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05,
611 | 0x61, 0x70, 0x70, 0x49, 0x44, 0x22, 0x5a, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72,
612 | 0x4c, 0x69, 0x73, 0x74, 0x52, 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x74, 0x43, 0x6f,
613 | 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x72, 0x65, 0x74, 0x43, 0x6f, 0x64,
614 | 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28,
615 | 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65,
616 | 0x72, 0x49, 0x44, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
617 | 0x44, 0x32, 0x9f, 0x02, 0x0a, 0x09, 0x41, 0x63, 0x63, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12,
618 | 0x52, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x73, 0x4f, 0x6e, 0x6c,
619 | 0x69, 0x6e, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x51,
620 | 0x75, 0x65, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x52,
621 | 0x65, 0x71, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x51, 0x75,
622 | 0x65, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x52, 0x73,
623 | 0x70, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x12, 0x14,
624 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73,
625 | 0x67, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
626 | 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x52, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0a,
627 | 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x41, 0x6c, 0x6c, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f,
628 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x41, 0x6c, 0x6c,
629 | 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53,
630 | 0x65, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x41, 0x6c, 0x6c, 0x52, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43,
631 | 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x2e,
632 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72,
633 | 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
634 | 0x75, 0x66, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x73,
635 | 0x70, 0x22, 0x00, 0x42, 0x39, 0x0a, 0x19, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x65,
636 | 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
637 | 0x42, 0x0d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
638 | 0x01, 0x5a, 0x0b, 0x2e, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06,
639 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
640 | }
641 |
642 | var (
643 | file_im_protobuf_proto_rawDescOnce sync.Once
644 | file_im_protobuf_proto_rawDescData = file_im_protobuf_proto_rawDesc
645 | )
646 |
647 | func file_im_protobuf_proto_rawDescGZIP() []byte {
648 | file_im_protobuf_proto_rawDescOnce.Do(func() {
649 | file_im_protobuf_proto_rawDescData = protoimpl.X.CompressGZIP(file_im_protobuf_proto_rawDescData)
650 | })
651 | return file_im_protobuf_proto_rawDescData
652 | }
653 |
654 | var file_im_protobuf_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
655 | var file_im_protobuf_proto_goTypes = []interface{}{
656 | (*QueryUsersOnlineReq)(nil), // 0: protobuf.QueryUsersOnlineReq
657 | (*QueryUsersOnlineRsp)(nil), // 1: protobuf.QueryUsersOnlineRsp
658 | (*SendMsgReq)(nil), // 2: protobuf.SendMsgReq
659 | (*SendMsgRsp)(nil), // 3: protobuf.SendMsgRsp
660 | (*SendMsgAllReq)(nil), // 4: protobuf.SendMsgAllReq
661 | (*SendMsgAllRsp)(nil), // 5: protobuf.SendMsgAllRsp
662 | (*GetUserListReq)(nil), // 6: protobuf.GetUserListReq
663 | (*GetUserListRsp)(nil), // 7: protobuf.GetUserListRsp
664 | }
665 | var file_im_protobuf_proto_depIdxs = []int32{
666 | 0, // 0: protobuf.AccServer.QueryUsersOnline:input_type -> protobuf.QueryUsersOnlineReq
667 | 2, // 1: protobuf.AccServer.SendMsg:input_type -> protobuf.SendMsgReq
668 | 4, // 2: protobuf.AccServer.SendMsgAll:input_type -> protobuf.SendMsgAllReq
669 | 6, // 3: protobuf.AccServer.GetUserList:input_type -> protobuf.GetUserListReq
670 | 1, // 4: protobuf.AccServer.QueryUsersOnline:output_type -> protobuf.QueryUsersOnlineRsp
671 | 3, // 5: protobuf.AccServer.SendMsg:output_type -> protobuf.SendMsgRsp
672 | 5, // 6: protobuf.AccServer.SendMsgAll:output_type -> protobuf.SendMsgAllRsp
673 | 7, // 7: protobuf.AccServer.GetUserList:output_type -> protobuf.GetUserListRsp
674 | 4, // [4:8] is the sub-list for method output_type
675 | 0, // [0:4] is the sub-list for method input_type
676 | 0, // [0:0] is the sub-list for extension type_name
677 | 0, // [0:0] is the sub-list for extension extendee
678 | 0, // [0:0] is the sub-list for field type_name
679 | }
680 |
681 | func init() { file_im_protobuf_proto_init() }
682 | func file_im_protobuf_proto_init() {
683 | if File_im_protobuf_proto != nil {
684 | return
685 | }
686 | if !protoimpl.UnsafeEnabled {
687 | file_im_protobuf_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
688 | switch v := v.(*QueryUsersOnlineReq); i {
689 | case 0:
690 | return &v.state
691 | case 1:
692 | return &v.sizeCache
693 | case 2:
694 | return &v.unknownFields
695 | default:
696 | return nil
697 | }
698 | }
699 | file_im_protobuf_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
700 | switch v := v.(*QueryUsersOnlineRsp); i {
701 | case 0:
702 | return &v.state
703 | case 1:
704 | return &v.sizeCache
705 | case 2:
706 | return &v.unknownFields
707 | default:
708 | return nil
709 | }
710 | }
711 | file_im_protobuf_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
712 | switch v := v.(*SendMsgReq); i {
713 | case 0:
714 | return &v.state
715 | case 1:
716 | return &v.sizeCache
717 | case 2:
718 | return &v.unknownFields
719 | default:
720 | return nil
721 | }
722 | }
723 | file_im_protobuf_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
724 | switch v := v.(*SendMsgRsp); i {
725 | case 0:
726 | return &v.state
727 | case 1:
728 | return &v.sizeCache
729 | case 2:
730 | return &v.unknownFields
731 | default:
732 | return nil
733 | }
734 | }
735 | file_im_protobuf_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
736 | switch v := v.(*SendMsgAllReq); i {
737 | case 0:
738 | return &v.state
739 | case 1:
740 | return &v.sizeCache
741 | case 2:
742 | return &v.unknownFields
743 | default:
744 | return nil
745 | }
746 | }
747 | file_im_protobuf_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
748 | switch v := v.(*SendMsgAllRsp); i {
749 | case 0:
750 | return &v.state
751 | case 1:
752 | return &v.sizeCache
753 | case 2:
754 | return &v.unknownFields
755 | default:
756 | return nil
757 | }
758 | }
759 | file_im_protobuf_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
760 | switch v := v.(*GetUserListReq); i {
761 | case 0:
762 | return &v.state
763 | case 1:
764 | return &v.sizeCache
765 | case 2:
766 | return &v.unknownFields
767 | default:
768 | return nil
769 | }
770 | }
771 | file_im_protobuf_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
772 | switch v := v.(*GetUserListRsp); i {
773 | case 0:
774 | return &v.state
775 | case 1:
776 | return &v.sizeCache
777 | case 2:
778 | return &v.unknownFields
779 | default:
780 | return nil
781 | }
782 | }
783 | }
784 | type x struct{}
785 | out := protoimpl.TypeBuilder{
786 | File: protoimpl.DescBuilder{
787 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
788 | RawDescriptor: file_im_protobuf_proto_rawDesc,
789 | NumEnums: 0,
790 | NumMessages: 8,
791 | NumExtensions: 0,
792 | NumServices: 1,
793 | },
794 | GoTypes: file_im_protobuf_proto_goTypes,
795 | DependencyIndexes: file_im_protobuf_proto_depIdxs,
796 | MessageInfos: file_im_protobuf_proto_msgTypes,
797 | }.Build()
798 | File_im_protobuf_proto = out.File
799 | file_im_protobuf_proto_rawDesc = nil
800 | file_im_protobuf_proto_goTypes = nil
801 | file_im_protobuf_proto_depIdxs = nil
802 | }
803 |
--------------------------------------------------------------------------------
/protobuf/im_protobuf.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "../protobuf";
4 | option java_multiple_files = true;
5 | option java_package = "io.grpc.examples.protobuf";
6 | option java_outer_classname = "ProtobufProto";
7 |
8 |
9 | package protobuf;
10 |
11 | // The AccServer service definition.
12 | service AccServer {
13 | // 查询用户是否在线
14 | rpc QueryUsersOnline (QueryUsersOnlineReq) returns (QueryUsersOnlineRsp) {
15 | }
16 | // 发送消息
17 | rpc SendMsg (SendMsgReq) returns (SendMsgRsp) {
18 | }
19 | // 给这台机器的房间内所有用户发送消息
20 | rpc SendMsgAll (SendMsgAllReq) returns (SendMsgAllRsp) {
21 | }
22 | // 获取用户列表
23 | rpc GetUserList (GetUserListReq) returns (GetUserListRsp) {
24 | }
25 | }
26 |
27 | // 查询用户是否在线
28 | message QueryUsersOnlineReq {
29 | uint32 appID = 1; // AppID
30 | string userID = 2; // 用户ID
31 | }
32 |
33 | message QueryUsersOnlineRsp {
34 | uint32 retCode = 1;
35 | string errMsg = 2;
36 | bool online = 3;
37 | }
38 |
39 | // 发送消息
40 | message SendMsgReq {
41 | string seq = 1; // 序列号
42 | uint32 appID = 2; // appID/房间ID
43 | string userID = 3; // 用户ID
44 | string cms = 4; // cms 动作: msg/enter/exit
45 | string type = 5; // type 消息类型,默认是 text
46 | string msg = 6; // msg
47 | bool isLocal = 7; // 是否查询本机 acc内部调用为:true(本机查询不到即结束)
48 | }
49 |
50 | message SendMsgRsp {
51 | uint32 retCode = 1;
52 | string errMsg = 2;
53 | string sendMsgID = 3;
54 | }
55 |
56 | // 给这台机器的房间内所有用户发送消息
57 | message SendMsgAllReq {
58 | string seq = 1; // 序列号
59 | uint32 appID = 2; // appID/房间ID
60 | string userID = 3; // 不发送的用户ID
61 | string cms = 4; // cms 动作: msg/enter/exit
62 | string type = 5; // type 消息类型,默认是 text
63 | string msg = 6; // msg
64 | }
65 |
66 | message SendMsgAllRsp {
67 | uint32 retCode = 1;
68 | string errMsg = 2;
69 | string sendMsgID = 3;
70 | }
71 |
72 | // 获取用户列表
73 | message GetUserListReq {
74 | uint32 appID = 1;
75 | }
76 |
77 | message GetUserListRsp {
78 | uint32 retCode = 1;
79 | string errMsg = 2;
80 | repeated string userID = 3;
81 | }
--------------------------------------------------------------------------------
/protobuf/im_protobuf_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.3.0
4 | // - protoc v4.24.3
5 | // source: im_protobuf.proto
6 |
7 | package protobuf
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.32.0 or later.
19 | const _ = grpc.SupportPackageIsVersion7
20 |
21 | const (
22 | AccServer_QueryUsersOnline_FullMethodName = "/protobuf.AccServer/QueryUsersOnline"
23 | AccServer_SendMsg_FullMethodName = "/protobuf.AccServer/SendMsg"
24 | AccServer_SendMsgAll_FullMethodName = "/protobuf.AccServer/SendMsgAll"
25 | AccServer_GetUserList_FullMethodName = "/protobuf.AccServer/GetUserList"
26 | )
27 |
28 | // AccServerClient is the client API for AccServer service.
29 | //
30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
31 | type AccServerClient interface {
32 | // 查询用户是否在线
33 | QueryUsersOnline(ctx context.Context, in *QueryUsersOnlineReq, opts ...grpc.CallOption) (*QueryUsersOnlineRsp, error)
34 | // 发送消息
35 | SendMsg(ctx context.Context, in *SendMsgReq, opts ...grpc.CallOption) (*SendMsgRsp, error)
36 | // 给这台机器的房间内所有用户发送消息
37 | SendMsgAll(ctx context.Context, in *SendMsgAllReq, opts ...grpc.CallOption) (*SendMsgAllRsp, error)
38 | // 获取用户列表
39 | GetUserList(ctx context.Context, in *GetUserListReq, opts ...grpc.CallOption) (*GetUserListRsp, error)
40 | }
41 |
42 | type accServerClient struct {
43 | cc grpc.ClientConnInterface
44 | }
45 |
46 | func NewAccServerClient(cc grpc.ClientConnInterface) AccServerClient {
47 | return &accServerClient{cc}
48 | }
49 |
50 | func (c *accServerClient) QueryUsersOnline(ctx context.Context, in *QueryUsersOnlineReq, opts ...grpc.CallOption) (*QueryUsersOnlineRsp, error) {
51 | out := new(QueryUsersOnlineRsp)
52 | err := c.cc.Invoke(ctx, AccServer_QueryUsersOnline_FullMethodName, in, out, opts...)
53 | if err != nil {
54 | return nil, err
55 | }
56 | return out, nil
57 | }
58 |
59 | func (c *accServerClient) SendMsg(ctx context.Context, in *SendMsgReq, opts ...grpc.CallOption) (*SendMsgRsp, error) {
60 | out := new(SendMsgRsp)
61 | err := c.cc.Invoke(ctx, AccServer_SendMsg_FullMethodName, in, out, opts...)
62 | if err != nil {
63 | return nil, err
64 | }
65 | return out, nil
66 | }
67 |
68 | func (c *accServerClient) SendMsgAll(ctx context.Context, in *SendMsgAllReq, opts ...grpc.CallOption) (*SendMsgAllRsp, error) {
69 | out := new(SendMsgAllRsp)
70 | err := c.cc.Invoke(ctx, AccServer_SendMsgAll_FullMethodName, in, out, opts...)
71 | if err != nil {
72 | return nil, err
73 | }
74 | return out, nil
75 | }
76 |
77 | func (c *accServerClient) GetUserList(ctx context.Context, in *GetUserListReq, opts ...grpc.CallOption) (*GetUserListRsp, error) {
78 | out := new(GetUserListRsp)
79 | err := c.cc.Invoke(ctx, AccServer_GetUserList_FullMethodName, in, out, opts...)
80 | if err != nil {
81 | return nil, err
82 | }
83 | return out, nil
84 | }
85 |
86 | // AccServerServer is the server API for AccServer service.
87 | // All implementations must embed UnimplementedAccServerServer
88 | // for forward compatibility
89 | type AccServerServer interface {
90 | // 查询用户是否在线
91 | QueryUsersOnline(context.Context, *QueryUsersOnlineReq) (*QueryUsersOnlineRsp, error)
92 | // 发送消息
93 | SendMsg(context.Context, *SendMsgReq) (*SendMsgRsp, error)
94 | // 给这台机器的房间内所有用户发送消息
95 | SendMsgAll(context.Context, *SendMsgAllReq) (*SendMsgAllRsp, error)
96 | // 获取用户列表
97 | GetUserList(context.Context, *GetUserListReq) (*GetUserListRsp, error)
98 | mustEmbedUnimplementedAccServerServer()
99 | }
100 |
101 | // UnimplementedAccServerServer must be embedded to have forward compatible implementations.
102 | type UnimplementedAccServerServer struct {
103 | }
104 |
105 | func (UnimplementedAccServerServer) QueryUsersOnline(context.Context, *QueryUsersOnlineReq) (*QueryUsersOnlineRsp, error) {
106 | return nil, status.Errorf(codes.Unimplemented, "method QueryUsersOnline not implemented")
107 | }
108 | func (UnimplementedAccServerServer) SendMsg(context.Context, *SendMsgReq) (*SendMsgRsp, error) {
109 | return nil, status.Errorf(codes.Unimplemented, "method SendMsg not implemented")
110 | }
111 | func (UnimplementedAccServerServer) SendMsgAll(context.Context, *SendMsgAllReq) (*SendMsgAllRsp, error) {
112 | return nil, status.Errorf(codes.Unimplemented, "method SendMsgAll not implemented")
113 | }
114 | func (UnimplementedAccServerServer) GetUserList(context.Context, *GetUserListReq) (*GetUserListRsp, error) {
115 | return nil, status.Errorf(codes.Unimplemented, "method GetUserList not implemented")
116 | }
117 | func (UnimplementedAccServerServer) mustEmbedUnimplementedAccServerServer() {}
118 |
119 | // UnsafeAccServerServer may be embedded to opt out of forward compatibility for this service.
120 | // Use of this interface is not recommended, as added methods to AccServerServer will
121 | // result in compilation errors.
122 | type UnsafeAccServerServer interface {
123 | mustEmbedUnimplementedAccServerServer()
124 | }
125 |
126 | func RegisterAccServerServer(s grpc.ServiceRegistrar, srv AccServerServer) {
127 | s.RegisterService(&AccServer_ServiceDesc, srv)
128 | }
129 |
130 | func _AccServer_QueryUsersOnline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
131 | in := new(QueryUsersOnlineReq)
132 | if err := dec(in); err != nil {
133 | return nil, err
134 | }
135 | if interceptor == nil {
136 | return srv.(AccServerServer).QueryUsersOnline(ctx, in)
137 | }
138 | info := &grpc.UnaryServerInfo{
139 | Server: srv,
140 | FullMethod: AccServer_QueryUsersOnline_FullMethodName,
141 | }
142 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
143 | return srv.(AccServerServer).QueryUsersOnline(ctx, req.(*QueryUsersOnlineReq))
144 | }
145 | return interceptor(ctx, in, info, handler)
146 | }
147 |
148 | func _AccServer_SendMsg_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
149 | in := new(SendMsgReq)
150 | if err := dec(in); err != nil {
151 | return nil, err
152 | }
153 | if interceptor == nil {
154 | return srv.(AccServerServer).SendMsg(ctx, in)
155 | }
156 | info := &grpc.UnaryServerInfo{
157 | Server: srv,
158 | FullMethod: AccServer_SendMsg_FullMethodName,
159 | }
160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
161 | return srv.(AccServerServer).SendMsg(ctx, req.(*SendMsgReq))
162 | }
163 | return interceptor(ctx, in, info, handler)
164 | }
165 |
166 | func _AccServer_SendMsgAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
167 | in := new(SendMsgAllReq)
168 | if err := dec(in); err != nil {
169 | return nil, err
170 | }
171 | if interceptor == nil {
172 | return srv.(AccServerServer).SendMsgAll(ctx, in)
173 | }
174 | info := &grpc.UnaryServerInfo{
175 | Server: srv,
176 | FullMethod: AccServer_SendMsgAll_FullMethodName,
177 | }
178 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
179 | return srv.(AccServerServer).SendMsgAll(ctx, req.(*SendMsgAllReq))
180 | }
181 | return interceptor(ctx, in, info, handler)
182 | }
183 |
184 | func _AccServer_GetUserList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
185 | in := new(GetUserListReq)
186 | if err := dec(in); err != nil {
187 | return nil, err
188 | }
189 | if interceptor == nil {
190 | return srv.(AccServerServer).GetUserList(ctx, in)
191 | }
192 | info := &grpc.UnaryServerInfo{
193 | Server: srv,
194 | FullMethod: AccServer_GetUserList_FullMethodName,
195 | }
196 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
197 | return srv.(AccServerServer).GetUserList(ctx, req.(*GetUserListReq))
198 | }
199 | return interceptor(ctx, in, info, handler)
200 | }
201 |
202 | // AccServer_ServiceDesc is the grpc.ServiceDesc for AccServer service.
203 | // It's only intended for direct use with grpc.RegisterService,
204 | // and not to be introspected or modified (even as a copy)
205 | var AccServer_ServiceDesc = grpc.ServiceDesc{
206 | ServiceName: "protobuf.AccServer",
207 | HandlerType: (*AccServerServer)(nil),
208 | Methods: []grpc.MethodDesc{
209 | {
210 | MethodName: "QueryUsersOnline",
211 | Handler: _AccServer_QueryUsersOnline_Handler,
212 | },
213 | {
214 | MethodName: "SendMsg",
215 | Handler: _AccServer_SendMsg_Handler,
216 | },
217 | {
218 | MethodName: "SendMsgAll",
219 | Handler: _AccServer_SendMsgAll_Handler,
220 | },
221 | {
222 | MethodName: "GetUserList",
223 | Handler: _AccServer_GetUserList_Handler,
224 | },
225 | },
226 | Streams: []grpc.StreamDesc{},
227 | Metadata: "im_protobuf.proto",
228 | }
229 |
--------------------------------------------------------------------------------
/routers/acc_routers.go:
--------------------------------------------------------------------------------
1 | // Package routers 路由
2 | package routers
3 |
4 | import (
5 | "github.com/link1st/gowebsocket/v2/servers/websocket"
6 | )
7 |
8 | // WebsocketInit Websocket 路由
9 | func WebsocketInit() {
10 | websocket.Register("login", websocket.LoginController)
11 | websocket.Register("heartbeat", websocket.HeartbeatController)
12 | websocket.Register("ping", websocket.PingController)
13 | }
14 |
--------------------------------------------------------------------------------
/routers/web_routers.go:
--------------------------------------------------------------------------------
1 | // Package routers 路由
2 | package routers
3 |
4 | import (
5 | "github.com/gin-gonic/gin"
6 |
7 | "github.com/link1st/gowebsocket/v2/controllers/home"
8 | "github.com/link1st/gowebsocket/v2/controllers/systems"
9 | "github.com/link1st/gowebsocket/v2/controllers/user"
10 | )
11 |
12 | // Init http 接口路由
13 | func Init(router *gin.Engine) {
14 | router.LoadHTMLGlob("views/**/*")
15 |
16 | // 用户组
17 | userRouter := router.Group("/user")
18 | {
19 | userRouter.GET("/list", user.List)
20 | userRouter.GET("/online", user.Online)
21 | userRouter.POST("/sendMessage", user.SendMessage)
22 | userRouter.POST("/sendMessageAll", user.SendMessageAll)
23 | }
24 |
25 | // 系统
26 | systemRouter := router.Group("/system")
27 | {
28 | systemRouter.GET("/state", systems.Status)
29 | }
30 |
31 | // home
32 | homeRouter := router.Group("/home")
33 | {
34 | homeRouter.GET("/index", home.Index)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/servers/grpcclient/grpc_client.go:
--------------------------------------------------------------------------------
1 | // Package grpcclient grpc 客户端
2 | package grpcclient
3 |
4 | import (
5 | "context"
6 | "errors"
7 | "fmt"
8 | "time"
9 |
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/credentials/insecure"
12 |
13 | "github.com/link1st/gowebsocket/v2/common"
14 | "github.com/link1st/gowebsocket/v2/models"
15 | "github.com/link1st/gowebsocket/v2/protobuf"
16 | )
17 |
18 | // SendMsgAll 给全体用户发送消息
19 | // link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
20 | func SendMsgAll(server *models.Server, seq string, appID uint32, userID string, cmd string,
21 | message string) (sendMsgID string, err error) {
22 | // Set up a connection to the server.
23 | conn, err := grpc.Dial(server.String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
24 | if err != nil {
25 | fmt.Println("连接失败", server.String())
26 |
27 | return
28 | }
29 | defer func() { _ = conn.Close() }()
30 | c := protobuf.NewAccServerClient(conn)
31 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
32 | defer cancel()
33 | req := protobuf.SendMsgAllReq{
34 | Seq: seq,
35 | AppID: appID,
36 | UserID: userID,
37 | Cms: cmd,
38 | Msg: message,
39 | }
40 | rsp, err := c.SendMsgAll(ctx, &req)
41 | if err != nil {
42 | fmt.Println("给全体用户发送消息", err)
43 |
44 | return
45 | }
46 | if rsp.GetRetCode() != common.OK {
47 | fmt.Println("给全体用户发送消息", rsp.String())
48 | err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
49 |
50 | return
51 | }
52 | sendMsgID = rsp.GetSendMsgID()
53 | fmt.Println("给全体用户发送消息 成功:", sendMsgID)
54 | return
55 | }
56 |
57 | // GetUserList 获取用户列表
58 | // link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
59 | func GetUserList(server *models.Server, appID uint32) (userIDs []string, err error) {
60 | userIDs = make([]string, 0)
61 | conn, err := grpc.Dial(server.String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
62 | if err != nil {
63 | fmt.Println("连接失败", server.String())
64 | return
65 | }
66 | defer func() { _ = conn.Close() }()
67 | c := protobuf.NewAccServerClient(conn)
68 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
69 | defer cancel()
70 | req := protobuf.GetUserListReq{
71 | AppID: appID,
72 | }
73 | rsp, err := c.GetUserList(ctx, &req)
74 | if err != nil {
75 | fmt.Println("获取用户列表 发送请求错误:", err)
76 | return
77 | }
78 | if rsp.GetRetCode() != common.OK {
79 | fmt.Println("获取用户列表 返回码错误:", rsp.String())
80 | err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
81 | return
82 | }
83 | userIDs = rsp.GetUserID()
84 | fmt.Println("获取用户列表 成功:", userIDs)
85 | return
86 | }
87 |
88 | // SendMsg 发送消息
89 | // link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
90 | func SendMsg(server *models.Server, seq string, appID uint32, userID string, cmd string, msgType string,
91 | message string) (sendMsgID string, err error) {
92 | // Set up a connection to the server.
93 | conn, err := grpc.Dial(server.String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
94 | if err != nil {
95 | fmt.Println("连接失败", server.String())
96 | return
97 | }
98 | defer func() { _ = conn.Close() }()
99 | c := protobuf.NewAccServerClient(conn)
100 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
101 | defer cancel()
102 | req := protobuf.SendMsgReq{
103 | Seq: seq,
104 | AppID: appID,
105 | UserID: userID,
106 | Cms: cmd,
107 | Type: msgType,
108 | Msg: message,
109 | IsLocal: false,
110 | }
111 | rsp, err := c.SendMsg(ctx, &req)
112 | if err != nil {
113 | fmt.Println("发送消息", err)
114 | return
115 | }
116 | if rsp.GetRetCode() != common.OK {
117 | fmt.Println("发送消息", rsp.String())
118 | err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
119 | return
120 | }
121 | sendMsgID = rsp.GetSendMsgID()
122 | fmt.Println("发送消息 成功:", sendMsgID)
123 | return
124 | }
125 |
--------------------------------------------------------------------------------
/servers/grpcserver/grpc_server.go:
--------------------------------------------------------------------------------
1 | // Package grpcserver grpc 服务器
2 | package grpcserver
3 |
4 | import (
5 | "context"
6 | "fmt"
7 | "log"
8 | "net"
9 |
10 | "github.com/link1st/gowebsocket/v2/common"
11 | "github.com/link1st/gowebsocket/v2/models"
12 | "github.com/link1st/gowebsocket/v2/protobuf"
13 | "github.com/link1st/gowebsocket/v2/servers/websocket"
14 |
15 | "github.com/golang/protobuf/proto"
16 | "github.com/spf13/viper"
17 | "google.golang.org/grpc"
18 | )
19 |
20 | type server struct {
21 | protobuf.UnimplementedAccServerServer
22 | }
23 |
24 | func setErr(rsp proto.Message, code uint32, message string) {
25 | message = common.GetErrorMessage(code, message)
26 | switch v := rsp.(type) {
27 | case *protobuf.QueryUsersOnlineRsp:
28 | v.RetCode = code
29 | v.ErrMsg = message
30 | case *protobuf.SendMsgRsp:
31 | v.RetCode = code
32 | v.ErrMsg = message
33 | case *protobuf.SendMsgAllRsp:
34 | v.RetCode = code
35 | v.ErrMsg = message
36 | case *protobuf.GetUserListRsp:
37 | v.RetCode = code
38 | v.ErrMsg = message
39 | default:
40 | }
41 | }
42 |
43 | // QueryUsersOnline 查询用户是否在线
44 | func (s *server) QueryUsersOnline(c context.Context,
45 | req *protobuf.QueryUsersOnlineReq) (rsp *protobuf.QueryUsersOnlineRsp, err error) {
46 |
47 | fmt.Println("grpc_request 查询用户是否在线", req.String())
48 |
49 | rsp = &protobuf.QueryUsersOnlineRsp{}
50 |
51 | online := websocket.CheckUserOnline(req.GetAppID(), req.GetUserID())
52 |
53 | setErr(req, common.OK, "")
54 | rsp.Online = online
55 |
56 | return rsp, nil
57 | }
58 |
59 | // SendMsg 给本机用户发消息
60 | func (s *server) SendMsg(c context.Context, req *protobuf.SendMsgReq) (rsp *protobuf.SendMsgRsp, err error) {
61 | fmt.Println("grpc_request 给本机用户发消息", req.String())
62 | rsp = &protobuf.SendMsgRsp{}
63 | if req.GetIsLocal() {
64 | // 不支持
65 | setErr(rsp, common.ParameterIllegal, "")
66 | return
67 | }
68 | data := models.GetMsgData(req.GetUserID(), req.GetSeq(), req.GetCms(), req.GetMsg())
69 | sendResults, err := websocket.SendUserMessageLocal(req.GetAppID(), req.GetUserID(), data)
70 | if err != nil {
71 | fmt.Println("系统错误", err)
72 | setErr(rsp, common.ServerError, "")
73 | return rsp, nil
74 | }
75 | if !sendResults {
76 | fmt.Println("发送失败", err)
77 | setErr(rsp, common.OperationFailure, "")
78 | return rsp, nil
79 | }
80 | setErr(rsp, common.OK, "")
81 | fmt.Println("grpc_response 给本机用户发消息", rsp.String())
82 | return
83 | }
84 |
85 | // SendMsgAll 给本机全体用户发消息
86 | func (s *server) SendMsgAll(c context.Context, req *protobuf.SendMsgAllReq) (rsp *protobuf.SendMsgAllRsp, err error) {
87 | fmt.Println("grpc_request 给本机全体用户发消息", req.String())
88 | rsp = &protobuf.SendMsgAllRsp{}
89 | data := models.GetMsgData(req.GetUserID(), req.GetSeq(), req.GetCms(), req.GetMsg())
90 | websocket.AllSendMessages(req.GetAppID(), req.GetUserID(), data)
91 | setErr(rsp, common.OK, "")
92 | fmt.Println("grpc_response 给本机全体用户发消息:", rsp.String())
93 | return
94 | }
95 |
96 | // GetUserList 获取本机用户列表
97 | func (s *server) GetUserList(c context.Context, req *protobuf.GetUserListReq) (rsp *protobuf.GetUserListRsp,
98 | err error) {
99 |
100 | fmt.Println("grpc_request 获取本机用户列表", req.String())
101 |
102 | appID := req.GetAppID()
103 | rsp = &protobuf.GetUserListRsp{}
104 |
105 | // 本机
106 | userList := websocket.GetUserList(appID)
107 |
108 | setErr(rsp, common.OK, "")
109 | rsp.UserID = userList
110 |
111 | fmt.Println("grpc_response 获取本机用户列表:", rsp.String())
112 |
113 | return
114 | }
115 |
116 | // Init rpc server
117 | // link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
118 | func Init() {
119 | rpcPort := viper.GetString("app.rpcPort")
120 | fmt.Println("rpc server 启动", rpcPort)
121 | lis, err := net.Listen("tcp", ":"+rpcPort)
122 | if err != nil {
123 | log.Fatalf("failed to listen: %v", err)
124 | }
125 | s := grpc.NewServer()
126 | protobuf.RegisterAccServerServer(s, &server{})
127 | if err := s.Serve(lis); err != nil {
128 | log.Fatalf("failed to serve: %v", err)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/servers/task/clean_connection _task.go:
--------------------------------------------------------------------------------
1 | // Package task 定时任务
2 | package task
3 |
4 | import (
5 | "fmt"
6 | "runtime/debug"
7 | "time"
8 |
9 | "github.com/link1st/gowebsocket/v2/servers/websocket"
10 | )
11 |
12 | // Init 初始化
13 | func Init() {
14 | Timer(3*time.Second, 30*time.Second, cleanConnection, "", nil, nil)
15 |
16 | }
17 |
18 | // cleanConnection 清理超时连接
19 | func cleanConnection(param interface{}) (result bool) {
20 | result = true
21 | defer func() {
22 | if r := recover(); r != nil {
23 | fmt.Println("ClearTimeoutConnections stop", r, string(debug.Stack()))
24 | }
25 | }()
26 | fmt.Println("定时任务,清理超时连接", param)
27 | websocket.ClearTimeoutConnections()
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/servers/task/server_task.go:
--------------------------------------------------------------------------------
1 | // Package task 定时任务
2 | package task
3 |
4 | import (
5 | "fmt"
6 | "runtime/debug"
7 | "time"
8 |
9 | "github.com/link1st/gowebsocket/v2/lib/cache"
10 | "github.com/link1st/gowebsocket/v2/servers/websocket"
11 | )
12 |
13 | // ServerInit 服务初始化
14 | func ServerInit() {
15 | Timer(2*time.Second, 60*time.Second, server, "", serverDefer, "")
16 | }
17 |
18 | // server 服务注册
19 | func server(param interface{}) (result bool) {
20 | result = true
21 | defer func() {
22 | if r := recover(); r != nil {
23 | fmt.Println("服务注册 stop", r, string(debug.Stack()))
24 | }
25 | }()
26 | s := websocket.GetServer()
27 | currentTime := uint64(time.Now().Unix())
28 | fmt.Println("定时任务,服务注册", param, s, currentTime)
29 | _ = cache.SetServerInfo(s, currentTime)
30 | return
31 | }
32 |
33 | // serverDefer 服务下线
34 | func serverDefer(param interface{}) (result bool) {
35 | defer func() {
36 | if r := recover(); r != nil {
37 | fmt.Println("服务下线 stop", r, string(debug.Stack()))
38 | }
39 | }()
40 | fmt.Println("服务下线", param)
41 | s := websocket.GetServer()
42 | _ = cache.DelServerInfo(s)
43 | return
44 | }
45 |
--------------------------------------------------------------------------------
/servers/task/task_init.go:
--------------------------------------------------------------------------------
1 | // Package task 定时任务
2 | package task
3 |
4 | import "time"
5 |
6 | // TimerFunc 定时任务函数
7 | type TimerFunc func(interface{}) bool
8 |
9 | // Timer 定时调用
10 | //
11 | // @delay 首次延时
12 | // @tick 间隔
13 | // @fun 定时执行function
14 | // @param fun参数
15 | func Timer(delay, tick time.Duration, fun TimerFunc, param interface{}, funcDefer TimerFunc, paramDefer interface{}) {
16 | go func() {
17 | defer func() {
18 | if funcDefer != nil {
19 | funcDefer(paramDefer)
20 | }
21 | }()
22 | if fun == nil {
23 | return
24 | }
25 | t := time.NewTimer(delay)
26 | defer t.Stop()
27 | for {
28 | select {
29 | case <-t.C:
30 | if fun(param) == false {
31 | return
32 | }
33 | t.Reset(tick)
34 | }
35 | }
36 | }()
37 | }
38 |
--------------------------------------------------------------------------------
/servers/websocket/acc_controller.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "time"
9 |
10 | "github.com/link1st/gowebsocket/v2/common"
11 | "github.com/link1st/gowebsocket/v2/lib/cache"
12 | "github.com/link1st/gowebsocket/v2/models"
13 |
14 | "github.com/redis/go-redis/v9"
15 | )
16 |
17 | // PingController ping
18 | func PingController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
19 | code = common.OK
20 | fmt.Println("webSocket_request ping接口", client.Addr, seq, message)
21 | data = "pong"
22 | return
23 | }
24 |
25 | // LoginController 用户登录
26 | func LoginController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
27 | code = common.OK
28 | currentTime := uint64(time.Now().Unix())
29 | request := &models.Login{}
30 | if err := json.Unmarshal(message, request); err != nil {
31 | code = common.ParameterIllegal
32 | fmt.Println("用户登录 解析数据失败", seq, err)
33 | return
34 | }
35 | fmt.Println("webSocket_request 用户登录", seq, "ServiceToken", request.ServiceToken)
36 |
37 | // TODO::进行用户权限认证,一般是客户端传入TOKEN,然后检验TOKEN是否合法,通过TOKEN解析出来用户ID
38 | // 本项目只是演示,所以直接过去客户端传入的用户ID
39 | if request.UserID == "" || len(request.UserID) >= 20 {
40 | code = common.UnauthorizedUserID
41 | fmt.Println("用户登录 非法的用户", seq, request.UserID)
42 | return
43 | }
44 | if !InAppIDs(request.AppID) {
45 | code = common.Unauthorized
46 | fmt.Println("用户登录 不支持的平台", seq, request.AppID)
47 | return
48 | }
49 | if client.IsLogin() {
50 | fmt.Println("用户登录 用户已经登录", client.AppID, client.UserID, seq)
51 | code = common.OperationFailure
52 | return
53 | }
54 | client.Login(request.AppID, request.UserID, currentTime)
55 |
56 | // 存储数据
57 | userOnline := models.UserLogin(serverIp, serverPort, request.AppID, request.UserID, client.Addr, currentTime)
58 | err := cache.SetUserOnlineInfo(client.GetKey(), userOnline)
59 | if err != nil {
60 | code = common.ServerError
61 | fmt.Println("用户登录 SetUserOnlineInfo", seq, err)
62 | return
63 | }
64 |
65 | // 用户登录
66 | login := &login{
67 | AppID: request.AppID,
68 | UserID: request.UserID,
69 | Client: client,
70 | }
71 | clientManager.Login <- login
72 | fmt.Println("用户登录 成功", seq, client.Addr, request.UserID)
73 |
74 | return
75 | }
76 |
77 | // HeartbeatController 心跳接口
78 | func HeartbeatController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
79 | code = common.OK
80 | currentTime := uint64(time.Now().Unix())
81 | request := &models.HeartBeat{}
82 | if err := json.Unmarshal(message, request); err != nil {
83 | code = common.ParameterIllegal
84 | fmt.Println("心跳接口 解析数据失败", seq, err)
85 | return
86 | }
87 | fmt.Println("webSocket_request 心跳接口", client.AppID, client.UserID)
88 | if !client.IsLogin() {
89 | fmt.Println("心跳接口 用户未登录", client.AppID, client.UserID, seq)
90 | code = common.NotLoggedIn
91 | return
92 | }
93 | userOnline, err := cache.GetUserOnlineInfo(client.GetKey())
94 | if err != nil {
95 | if errors.Is(err, redis.Nil) {
96 | code = common.NotLoggedIn
97 | fmt.Println("心跳接口 用户未登录", seq, client.AppID, client.UserID)
98 | return
99 | } else {
100 | code = common.ServerError
101 | fmt.Println("心跳接口 GetUserOnlineInfo", seq, client.AppID, client.UserID, err)
102 | return
103 | }
104 | }
105 | client.Heartbeat(currentTime)
106 | userOnline.Heartbeat(currentTime)
107 | err = cache.SetUserOnlineInfo(client.GetKey(), userOnline)
108 | if err != nil {
109 | code = common.ServerError
110 | fmt.Println("心跳接口 SetUserOnlineInfo", seq, client.AppID, client.UserID, err)
111 | return
112 | }
113 | return
114 | }
115 |
--------------------------------------------------------------------------------
/servers/websocket/acc_process.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "encoding/json"
6 | "fmt"
7 | "sync"
8 |
9 | "github.com/link1st/gowebsocket/v2/common"
10 | "github.com/link1st/gowebsocket/v2/models"
11 | )
12 |
13 | // DisposeFunc 处理函数
14 | type DisposeFunc func(client *Client, seq string, message []byte) (code uint32, msg string, data interface{})
15 |
16 | var (
17 | handlers = make(map[string]DisposeFunc)
18 | handlersRWMutex sync.RWMutex
19 | )
20 |
21 | // Register 注册
22 | func Register(key string, value DisposeFunc) {
23 | handlersRWMutex.Lock()
24 | defer handlersRWMutex.Unlock()
25 | handlers[key] = value
26 | return
27 | }
28 |
29 | func getHandlers(key string) (value DisposeFunc, ok bool) {
30 | handlersRWMutex.RLock()
31 | defer handlersRWMutex.RUnlock()
32 | value, ok = handlers[key]
33 | return
34 | }
35 |
36 | // ProcessData 处理数据
37 | func ProcessData(client *Client, message []byte) {
38 | fmt.Println("处理数据", client.Addr, string(message))
39 | defer func() {
40 | if r := recover(); r != nil {
41 | fmt.Println("处理数据 stop", r)
42 | }
43 | }()
44 | request := &models.Request{}
45 | if err := json.Unmarshal(message, request); err != nil {
46 | fmt.Println("处理数据 json Unmarshal", err)
47 | client.SendMsg([]byte("数据不合法"))
48 | return
49 | }
50 | requestData, err := json.Marshal(request.Data)
51 | if err != nil {
52 | fmt.Println("处理数据 json Marshal", err)
53 | client.SendMsg([]byte("处理数据失败"))
54 | return
55 | }
56 | seq := request.Seq
57 | cmd := request.Cmd
58 | var (
59 | code uint32
60 | msg string
61 | data interface{}
62 | )
63 |
64 | // request
65 | fmt.Println("acc_request", cmd, client.Addr)
66 |
67 | // 采用 map 注册的方式
68 | if value, ok := getHandlers(cmd); ok {
69 | code, msg, data = value(client, seq, requestData)
70 | } else {
71 | code = common.RoutingNotExist
72 | fmt.Println("处理数据 路由不存在", client.Addr, "cmd", cmd)
73 | }
74 | msg = common.GetErrorMessage(code, msg)
75 | responseHead := models.NewResponseHead(seq, cmd, code, msg, data)
76 | headByte, err := json.Marshal(responseHead)
77 | if err != nil {
78 | fmt.Println("处理数据 json Marshal", err)
79 | return
80 | }
81 | client.SendMsg(headByte)
82 | fmt.Println("acc_response send", client.Addr, client.AppID, client.UserID, "cmd", cmd, "code", code)
83 | return
84 | }
85 |
--------------------------------------------------------------------------------
/servers/websocket/client.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "fmt"
6 | "runtime/debug"
7 |
8 | "github.com/gorilla/websocket"
9 | )
10 |
11 | const (
12 | // 用户连接超时时间
13 | heartbeatExpirationTime = 6 * 60
14 | )
15 |
16 | // 用户登录
17 | type login struct {
18 | AppID uint32
19 | UserID string
20 | Client *Client
21 | }
22 |
23 | // GetKey 获取 key
24 | func (l *login) GetKey() (key string) {
25 | key = GetUserKey(l.AppID, l.UserID)
26 |
27 | return
28 | }
29 |
30 | // Client 用户连接
31 | type Client struct {
32 | Addr string // 客户端地址
33 | Socket *websocket.Conn // 用户连接
34 | Send chan []byte // 待发送的数据
35 | AppID uint32 // 登录的平台ID app/web/ios
36 | UserID string // 用户ID,用户登录以后才有
37 | FirstTime uint64 // 首次连接事件
38 | HeartbeatTime uint64 // 用户上次心跳时间
39 | LoginTime uint64 // 登录时间 登录以后才有
40 | }
41 |
42 | // NewClient 初始化
43 | func NewClient(addr string, socket *websocket.Conn, firstTime uint64) (client *Client) {
44 | client = &Client{
45 | Addr: addr,
46 | Socket: socket,
47 | Send: make(chan []byte, 100),
48 | FirstTime: firstTime,
49 | HeartbeatTime: firstTime,
50 | }
51 | return
52 | }
53 |
54 | // GetKey 获取 key
55 | func (c *Client) GetKey() (key string) {
56 | key = GetUserKey(c.AppID, c.UserID)
57 | return
58 | }
59 |
60 | // 读取客户端数据
61 | func (c *Client) read() {
62 | defer func() {
63 | if r := recover(); r != nil {
64 | fmt.Println("write stop", string(debug.Stack()), r)
65 | }
66 | }()
67 | defer func() {
68 | fmt.Println("读取客户端数据 关闭send", c)
69 | close(c.Send)
70 | }()
71 | for {
72 | _, message, err := c.Socket.ReadMessage()
73 | if err != nil {
74 | fmt.Println("读取客户端数据 错误", c.Addr, err)
75 | return
76 | }
77 |
78 | // 处理程序
79 | fmt.Println("读取客户端数据 处理:", string(message))
80 | ProcessData(c, message)
81 | }
82 | }
83 |
84 | // 向客户端写数据
85 | func (c *Client) write() {
86 | defer func() {
87 | if r := recover(); r != nil {
88 | fmt.Println("write stop", string(debug.Stack()), r)
89 | }
90 | }()
91 | defer func() {
92 | clientManager.Unregister <- c
93 | _ = c.Socket.Close()
94 | fmt.Println("Client发送数据 defer", c)
95 | }()
96 | for {
97 | select {
98 | case message, ok := <-c.Send:
99 | if !ok {
100 | // 发送数据错误 关闭连接
101 | fmt.Println("Client发送数据 关闭连接", c.Addr, "ok", ok)
102 | return
103 | }
104 | _ = c.Socket.WriteMessage(websocket.TextMessage, message)
105 | }
106 | }
107 | }
108 |
109 | // SendMsg 发送数据
110 | func (c *Client) SendMsg(msg []byte) {
111 | if c == nil {
112 | return
113 | }
114 | defer func() {
115 | if r := recover(); r != nil {
116 | fmt.Println("SendMsg stop:", r, string(debug.Stack()))
117 | }
118 | }()
119 | c.Send <- msg
120 | }
121 |
122 | // close 关闭客户端连接
123 | func (c *Client) close() {
124 | close(c.Send)
125 | }
126 |
127 | // Login 用户登录
128 | func (c *Client) Login(appID uint32, userID string, loginTime uint64) {
129 | c.AppID = appID
130 | c.UserID = userID
131 | c.LoginTime = loginTime
132 | // 登录成功=心跳一次
133 | c.Heartbeat(loginTime)
134 | }
135 |
136 | // Heartbeat 用户心跳
137 | func (c *Client) Heartbeat(currentTime uint64) {
138 | c.HeartbeatTime = currentTime
139 |
140 | return
141 | }
142 |
143 | // IsHeartbeatTimeout 心跳超时
144 | func (c *Client) IsHeartbeatTimeout(currentTime uint64) (timeout bool) {
145 | if c.HeartbeatTime+heartbeatExpirationTime <= currentTime {
146 | timeout = true
147 | }
148 | return
149 | }
150 |
151 | // IsLogin 是否登录了
152 | func (c *Client) IsLogin() (isLogin bool) {
153 | // 用户登录了
154 | if c.UserID != "" {
155 | isLogin = true
156 | return
157 | }
158 | return
159 | }
160 |
--------------------------------------------------------------------------------
/servers/websocket/client_manager.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "fmt"
6 | "sync"
7 | "time"
8 |
9 | "github.com/link1st/gowebsocket/v2/helper"
10 | "github.com/link1st/gowebsocket/v2/lib/cache"
11 | "github.com/link1st/gowebsocket/v2/models"
12 | )
13 |
14 | // ClientManager 连接管理
15 | type ClientManager struct {
16 | Clients map[*Client]bool // 全部的连接
17 | ClientsLock sync.RWMutex // 读写锁
18 | Users map[string]*Client // 登录的用户 // appID+uuid
19 | UserLock sync.RWMutex // 读写锁
20 | Register chan *Client // 连接连接处理
21 | Login chan *login // 用户登录处理
22 | Unregister chan *Client // 断开连接处理程序
23 | Broadcast chan []byte // 广播 向全部成员发送数据
24 | }
25 |
26 | // NewClientManager 创建连接管理
27 | func NewClientManager() (clientManager *ClientManager) {
28 | clientManager = &ClientManager{
29 | Clients: make(map[*Client]bool),
30 | Users: make(map[string]*Client),
31 | Register: make(chan *Client, 1000),
32 | Login: make(chan *login, 1000),
33 | Unregister: make(chan *Client, 1000),
34 | Broadcast: make(chan []byte, 1000),
35 | }
36 | return
37 | }
38 |
39 | // GetUserKey 获取用户key
40 | func GetUserKey(appID uint32, userID string) (key string) {
41 | key = fmt.Sprintf("%d_%s", appID, userID)
42 | return
43 | }
44 |
45 | func (manager *ClientManager) InClient(client *Client) (ok bool) {
46 | manager.ClientsLock.RLock()
47 | defer manager.ClientsLock.RUnlock()
48 |
49 | // 连接存在,在添加
50 | _, ok = manager.Clients[client]
51 | return
52 | }
53 |
54 | // GetClients 获取所有客户端
55 | func (manager *ClientManager) GetClients() (clients map[*Client]bool) {
56 | clients = make(map[*Client]bool)
57 | manager.ClientsRange(func(client *Client, value bool) (result bool) {
58 | clients[client] = value
59 | return true
60 | })
61 | return
62 | }
63 |
64 | // ClientsRange 遍历
65 | func (manager *ClientManager) ClientsRange(f func(client *Client, value bool) (result bool)) {
66 | manager.ClientsLock.RLock()
67 | defer manager.ClientsLock.RUnlock()
68 | for key, value := range manager.Clients {
69 | result := f(key, value)
70 | if result == false {
71 | return
72 | }
73 | }
74 | return
75 | }
76 |
77 | // GetClientsLen GetClientsLen
78 | func (manager *ClientManager) GetClientsLen() (clientsLen int) {
79 | clientsLen = len(manager.Clients)
80 | return
81 | }
82 |
83 | // AddClients 添加客户端
84 | func (manager *ClientManager) AddClients(client *Client) {
85 | manager.ClientsLock.Lock()
86 | defer manager.ClientsLock.Unlock()
87 | manager.Clients[client] = true
88 | }
89 |
90 | // DelClients 删除客户端
91 | func (manager *ClientManager) DelClients(client *Client) {
92 | manager.ClientsLock.Lock()
93 | defer manager.ClientsLock.Unlock()
94 | if _, ok := manager.Clients[client]; ok {
95 | delete(manager.Clients, client)
96 | }
97 | }
98 |
99 | // GetUserClient 获取用户的连接
100 | func (manager *ClientManager) GetUserClient(appID uint32, userID string) (client *Client) {
101 | manager.UserLock.RLock()
102 | defer manager.UserLock.RUnlock()
103 | userKey := GetUserKey(appID, userID)
104 | if value, ok := manager.Users[userKey]; ok {
105 | client = value
106 | }
107 | return
108 | }
109 |
110 | // GetUsersLen GetClientsLen
111 | func (manager *ClientManager) GetUsersLen() (userLen int) {
112 | userLen = len(manager.Users)
113 | return
114 | }
115 |
116 | // AddUsers 添加用户
117 | func (manager *ClientManager) AddUsers(key string, client *Client) {
118 | manager.UserLock.Lock()
119 | defer manager.UserLock.Unlock()
120 | manager.Users[key] = client
121 | }
122 |
123 | // DelUsers 删除用户
124 | func (manager *ClientManager) DelUsers(client *Client) (result bool) {
125 | manager.UserLock.Lock()
126 | defer manager.UserLock.Unlock()
127 | key := GetUserKey(client.AppID, client.UserID)
128 | if value, ok := manager.Users[key]; ok {
129 | // 判断是否为相同的用户
130 | if value.Addr != client.Addr {
131 | return
132 | }
133 | delete(manager.Users, key)
134 | result = true
135 | }
136 | return
137 | }
138 |
139 | // GetUserKeys 获取用户的key
140 | func (manager *ClientManager) GetUserKeys() (userKeys []string) {
141 | userKeys = make([]string, 0, len(manager.Users))
142 | for key := range manager.Users {
143 | userKeys = append(userKeys, key)
144 | }
145 | return
146 | }
147 |
148 | // GetUserList 获取用户 list
149 | func (manager *ClientManager) GetUserList(appID uint32) (userList []string) {
150 | userList = make([]string, 0)
151 | manager.UserLock.RLock()
152 | defer manager.UserLock.RUnlock()
153 | for _, v := range manager.Users {
154 | if v.AppID == appID {
155 | userList = append(userList, v.UserID)
156 | }
157 | }
158 | fmt.Println("GetUserList len:", len(manager.Users))
159 | return
160 | }
161 |
162 | // GetUserClients 获取用户的key
163 | func (manager *ClientManager) GetUserClients() (clients []*Client) {
164 | clients = make([]*Client, 0)
165 | manager.UserLock.RLock()
166 | defer manager.UserLock.RUnlock()
167 | for _, v := range manager.Users {
168 | clients = append(clients, v)
169 | }
170 | return
171 | }
172 |
173 | // sendAll 向全部成员(除了自己)发送数据
174 | func (manager *ClientManager) sendAll(message []byte, ignoreClient *Client) {
175 | clients := manager.GetUserClients()
176 | for _, conn := range clients {
177 | if conn != ignoreClient {
178 | conn.SendMsg(message)
179 | }
180 | }
181 | }
182 |
183 | // sendAppIDAll 向全部成员(除了自己)发送数据
184 | func (manager *ClientManager) sendAppIDAll(message []byte, appID uint32, ignoreClient *Client) {
185 | clients := manager.GetUserClients()
186 | for _, conn := range clients {
187 | if conn != ignoreClient && conn.AppID == appID {
188 | conn.SendMsg(message)
189 | }
190 | }
191 | }
192 |
193 | // EventRegister 用户建立连接事件
194 | func (manager *ClientManager) EventRegister(client *Client) {
195 | manager.AddClients(client)
196 | fmt.Println("EventRegister 用户建立连接", client.Addr)
197 | // client.Send <- []byte("连接成功")
198 | }
199 |
200 | // EventLogin 用户登录
201 | func (manager *ClientManager) EventLogin(login *login) {
202 | client := login.Client
203 | // 连接存在,在添加
204 | if manager.InClient(client) {
205 | userKey := login.GetKey()
206 | manager.AddUsers(userKey, login.Client)
207 | }
208 | fmt.Println("EventLogin 用户登录", client.Addr, login.AppID, login.UserID)
209 | orderID := helper.GetOrderIDTime()
210 | _, _ = SendUserMessageAll(login.AppID, login.UserID, orderID, models.MessageCmdEnter, "哈喽~")
211 | }
212 |
213 | // EventUnregister 用户断开连接
214 | func (manager *ClientManager) EventUnregister(client *Client) {
215 | manager.DelClients(client)
216 |
217 | // 删除用户连接
218 | deleteResult := manager.DelUsers(client)
219 | if deleteResult == false {
220 | // 不是当前连接的客户端
221 | return
222 | }
223 |
224 | // 清除redis登录数据
225 | userOnline, err := cache.GetUserOnlineInfo(client.GetKey())
226 | if err == nil {
227 | userOnline.LogOut()
228 | _ = cache.SetUserOnlineInfo(client.GetKey(), userOnline)
229 | }
230 |
231 | // 关闭 chan
232 | // close(client.Send)
233 | fmt.Println("EventUnregister 用户断开连接", client.Addr, client.AppID, client.UserID)
234 | if client.UserID != "" {
235 | orderID := helper.GetOrderIDTime()
236 | _, _ = SendUserMessageAll(client.AppID, client.UserID, orderID, models.MessageCmdExit, "用户已经离开~")
237 | }
238 | }
239 |
240 | // 管道处理程序
241 | func (manager *ClientManager) start() {
242 | for {
243 | select {
244 | case conn := <-manager.Register:
245 | // 建立连接事件
246 | manager.EventRegister(conn)
247 | case l := <-manager.Login:
248 | // 用户登录
249 | manager.EventLogin(l)
250 | case conn := <-manager.Unregister:
251 | // 断开连接事件
252 | manager.EventUnregister(conn)
253 | case message := <-manager.Broadcast:
254 | // 广播事件
255 | clients := manager.GetClients()
256 | for conn := range clients {
257 | select {
258 | case conn.Send <- message:
259 | default:
260 | close(conn.Send)
261 | }
262 | }
263 | }
264 | }
265 | }
266 |
267 | // GetManagerInfo 获取管理者信息
268 | func GetManagerInfo(isDebug string) (managerInfo map[string]interface{}) {
269 | managerInfo = make(map[string]interface{})
270 | managerInfo["clientsLen"] = clientManager.GetClientsLen() // 客户端连接数
271 | managerInfo["usersLen"] = clientManager.GetUsersLen() // 登录用户数
272 | managerInfo["chanRegisterLen"] = len(clientManager.Register) // 未处理连接事件数
273 | managerInfo["chanLoginLen"] = len(clientManager.Login) // 未处理登录事件数
274 | managerInfo["chanUnregisterLen"] = len(clientManager.Unregister) // 未处理退出登录事件数
275 | managerInfo["chanBroadcastLen"] = len(clientManager.Broadcast) // 未处理广播事件数
276 | if isDebug == "true" {
277 | addrList := make([]string, 0)
278 | clientManager.ClientsRange(func(client *Client, value bool) (result bool) {
279 | addrList = append(addrList, client.Addr)
280 | return true
281 | })
282 | users := clientManager.GetUserKeys()
283 | managerInfo["clients"] = addrList // 客户端列表
284 | managerInfo["users"] = users // 登录用户列表
285 | }
286 | return
287 | }
288 |
289 | // GetUserClient 获取用户所在的连接
290 | func GetUserClient(appID uint32, userID string) (client *Client) {
291 | client = clientManager.GetUserClient(appID, userID)
292 | return
293 | }
294 |
295 | // ClearTimeoutConnections 定时清理超时连接
296 | func ClearTimeoutConnections() {
297 | currentTime := uint64(time.Now().Unix())
298 | clients := clientManager.GetClients()
299 | for client := range clients {
300 | if client.IsHeartbeatTimeout(currentTime) {
301 | fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserID, client.LoginTime, client.HeartbeatTime)
302 | _ = client.Socket.Close()
303 | }
304 | }
305 | }
306 |
307 | // GetUserList 获取全部用户
308 | func GetUserList(appID uint32) (userList []string) {
309 | fmt.Println("获取全部用户", appID)
310 | userList = clientManager.GetUserList(appID)
311 | return
312 | }
313 |
314 | // AllSendMessages 全员广播
315 | func AllSendMessages(appID uint32, userID string, data string) {
316 | fmt.Println("全员广播", appID, userID, data)
317 | ignoreClient := clientManager.GetUserClient(appID, userID)
318 | clientManager.sendAppIDAll([]byte(data), appID, ignoreClient)
319 | }
320 |
--------------------------------------------------------------------------------
/servers/websocket/init_acc.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "fmt"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/link1st/gowebsocket/v2/helper"
10 | "github.com/link1st/gowebsocket/v2/models"
11 |
12 | "github.com/gorilla/websocket"
13 | "github.com/spf13/viper"
14 | )
15 |
16 | const (
17 | defaultAppID = 101 // 默认平台ID
18 | )
19 |
20 | var (
21 | clientManager = NewClientManager() // 管理者
22 | appIDs = []uint32{defaultAppID, 102, 103, 104} // 全部的平台
23 | serverIp string
24 | serverPort string
25 | )
26 |
27 | // GetAppIDs 所有平台
28 | func GetAppIDs() []uint32 {
29 | return appIDs
30 | }
31 |
32 | // GetServer 获取服务器
33 | func GetServer() (server *models.Server) {
34 | server = models.NewServer(serverIp, serverPort)
35 | return
36 | }
37 |
38 | // IsLocal 判断是否为本机
39 | func IsLocal(server *models.Server) (isLocal bool) {
40 | if server.Ip == serverIp && server.Port == serverPort {
41 | isLocal = true
42 | }
43 | return
44 | }
45 |
46 | // InAppIDs in app
47 | func InAppIDs(appID uint32) (inAppID bool) {
48 | for _, value := range appIDs {
49 | if value == appID {
50 | inAppID = true
51 | return
52 | }
53 | }
54 | return
55 | }
56 |
57 | // GetDefaultAppID 获取默认 appID
58 | func GetDefaultAppID() (appID uint32) {
59 | appID = defaultAppID
60 | return
61 | }
62 |
63 | // StartWebSocket 启动程序
64 | func StartWebSocket() {
65 | serverIp = helper.GetServerIp()
66 | webSocketPort := viper.GetString("app.webSocketPort")
67 | rpcPort := viper.GetString("app.rpcPort")
68 | serverPort = rpcPort
69 | http.HandleFunc("/acc", wsPage)
70 |
71 | // 添加处理程序
72 | go clientManager.start()
73 | fmt.Println("WebSocket 启动程序成功", serverIp, serverPort)
74 | _ = http.ListenAndServe(":"+webSocketPort, nil)
75 | }
76 |
77 | func wsPage(w http.ResponseWriter, req *http.Request) {
78 |
79 | // 升级协议
80 | conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
81 | fmt.Println("升级协议", "ua:", r.Header["User-Agent"], "referer:", r.Header["Referer"])
82 | return true
83 | }}).Upgrade(w, req, nil)
84 | if err != nil {
85 | http.NotFound(w, req)
86 | return
87 | }
88 | fmt.Println("webSocket 建立连接:", conn.RemoteAddr().String())
89 | currentTime := uint64(time.Now().Unix())
90 | client := NewClient(conn.RemoteAddr().String(), conn, currentTime)
91 | go client.read()
92 | go client.write()
93 |
94 | // 用户连接事件
95 | clientManager.Register <- client
96 | }
97 |
--------------------------------------------------------------------------------
/servers/websocket/user_srv.go:
--------------------------------------------------------------------------------
1 | // Package websocket 处理
2 | package websocket
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/link1st/gowebsocket/v2/lib/cache"
10 | "github.com/link1st/gowebsocket/v2/models"
11 | "github.com/link1st/gowebsocket/v2/servers/grpcclient"
12 |
13 | "github.com/redis/go-redis/v9"
14 | )
15 |
16 | // UserList 查询所有用户
17 | func UserList(appID uint32) (userList []string) {
18 | userList = make([]string, 0)
19 | currentTime := uint64(time.Now().Unix())
20 | servers, err := cache.GetServerAll(currentTime)
21 | if err != nil {
22 | fmt.Println("给全体用户发消息", err)
23 | return
24 | }
25 | for _, server := range servers {
26 | var (
27 | list []string
28 | )
29 | if IsLocal(server) {
30 | list = GetUserList(appID)
31 | } else {
32 | list, _ = grpcclient.GetUserList(server, appID)
33 | }
34 | userList = append(userList, list...)
35 | }
36 | return
37 | }
38 |
39 | // CheckUserOnline 查询用户是否在线
40 | func CheckUserOnline(appID uint32, userID string) (online bool) {
41 | // 全平台查询
42 | if appID == 0 {
43 | for _, appID := range GetAppIDs() {
44 | online, _ = checkUserOnline(appID, userID)
45 | if online == true {
46 | break
47 | }
48 | }
49 | } else {
50 | online, _ = checkUserOnline(appID, userID)
51 | }
52 | return
53 | }
54 |
55 | // checkUserOnline 查询用户 是否在线
56 | func checkUserOnline(appID uint32, userID string) (online bool, err error) {
57 | key := GetUserKey(appID, userID)
58 | userOnline, err := cache.GetUserOnlineInfo(key)
59 | if err != nil {
60 | if errors.Is(err, redis.Nil) {
61 | fmt.Println("GetUserOnlineInfo", appID, userID, err)
62 | return false, nil
63 | }
64 | fmt.Println("GetUserOnlineInfo", appID, userID, err)
65 | return
66 | }
67 | online = userOnline.IsOnline()
68 | return
69 | }
70 |
71 | // SendUserMessage 给用户发送消息
72 | func SendUserMessage(appID uint32, userID string, msgID, message string) (sendResults bool, err error) {
73 | data := models.GetTextMsgData(userID, msgID, message)
74 | client := GetUserClient(appID, userID)
75 | if client != nil {
76 | // 在本机发送
77 | sendResults, err = SendUserMessageLocal(appID, userID, data)
78 | if err != nil {
79 | fmt.Println("给用户发送消息", appID, userID, err)
80 | }
81 | return
82 | }
83 | key := GetUserKey(appID, userID)
84 | info, err := cache.GetUserOnlineInfo(key)
85 | if err != nil {
86 | fmt.Println("给用户发送消息失败", key, err)
87 | return false, nil
88 | }
89 | if !info.IsOnline() {
90 | fmt.Println("用户不在线", key)
91 | return false, nil
92 | }
93 | server := models.NewServer(info.AccIp, info.AccPort)
94 | msg, err := grpcclient.SendMsg(server, msgID, appID, userID, models.MessageCmdMsg, models.MessageCmdMsg, message)
95 | if err != nil {
96 | fmt.Println("给用户发送消息失败", key, err)
97 | return false, err
98 | }
99 | fmt.Println("给用户发送消息成功-rpc", msg)
100 | sendResults = true
101 | return
102 | }
103 |
104 | // SendUserMessageLocal 给本机用户发送消息
105 | func SendUserMessageLocal(appID uint32, userID string, data string) (sendResults bool, err error) {
106 | client := GetUserClient(appID, userID)
107 | if client == nil {
108 | err = errors.New("用户不在线")
109 | return
110 | }
111 |
112 | // 发送消息
113 | client.SendMsg([]byte(data))
114 | sendResults = true
115 | return
116 | }
117 |
118 | // SendUserMessageAll 给全体用户发消息
119 | func SendUserMessageAll(appID uint32, userID string, msgID, cmd, message string) (sendResults bool, err error) {
120 | sendResults = true
121 | currentTime := uint64(time.Now().Unix())
122 | servers, err := cache.GetServerAll(currentTime)
123 | if err != nil {
124 | fmt.Println("给全体用户发消息", err)
125 | return
126 | }
127 | for _, server := range servers {
128 | if IsLocal(server) {
129 | data := models.GetMsgData(userID, msgID, cmd, message)
130 | AllSendMessages(appID, userID, data)
131 | } else {
132 | _, _ = grpcclient.SendMsgAll(server, msgID, appID, userID, cmd, message)
133 | }
134 | }
135 | return
136 | }
137 |
--------------------------------------------------------------------------------
/views/home/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ .title }}
6 |
221 |
222 |
223 |
224 |
225 |
226 |
235 |
236 |
237 |
238 | 管理员
239 |
240 |
241 |
242 | 欢迎加入聊天~
243 |
244 |
245 |
246 |
247 |
249 |
253 |
254 |
255 |
256 |
456 |
457 |
458 |
459 |
--------------------------------------------------------------------------------
/views/home/index.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ .title }}--房间ID({{ .appID }})
6 |
233 |
234 |
235 |
236 |
237 |
238 |
247 |
248 |
249 |
250 | 管理员
251 |
252 |
253 |
254 | 欢迎加入聊天~
255 |
256 |
257 |
258 |
259 |
261 |
265 |
266 |
267 |
276 |
277 |
278 |
490 |
491 |
492 |
493 |
--------------------------------------------------------------------------------
/目录.md:
--------------------------------------------------------------------------------
1 | ## 目录
2 | - 1、项目说明
3 | - 1.1 goWebSocket
4 | - 1.2 项目体验
5 | - 2、介绍webSocket
6 | - 2.1 webSocket 是什么
7 | - 2.2 webSocket的兼容性
8 | - 2.3 为什么要用webSocket
9 | - 2.4 webSocket建立过程
10 | - 3、如何实现基于webSocket的长连接系统
11 | - 3.1 使用go实现webSocket服务端
12 | - 3.1.1 启动端口监听
13 | - 3.1.2 升级协议
14 | - 3.1.3 客户端连接的管理
15 | - 3.1.4 注册客户端的socket的写的异步处理程序
16 | - 3.1.5 注册客户端的socket的读的异步处理程序
17 | - 3.1.6 接收客户端数据并处理
18 | - 3.1.7 使用路由的方式处理客户端的请求数据
19 | - 3.1.8 防止内存溢出和Goroutine不回收
20 | - 3.2 使用javaScript实现webSocket客户端
21 | - 3.2.1 启动并注册监听程序
22 | - 3.2.2 发送数据
23 | - 3.3 发送消息
24 | - 3.3.1 文本消息
25 | - 3.3.2 图片和语言消息
26 | - 4、goWebSocket 项目
27 | - 4.1 项目说明
28 | - 4.2 项目依赖
29 | - 4.3 项目启动
30 | - 4.4 接口文档
31 | - 4.4.1 HTTP接口文档
32 | - 4.4.1.1 接口说明
33 | - 4.4.1.2 聊天页面
34 | - 4.4.1.3 获取房间用户列表
35 | - 4.4.1.4 查询用户是否在线
36 | - 4.4.1.5 给用户发送消息
37 | - 4.4.1.6 给全员用户发送消息
38 | - 4.4.2 RPC接口文档
39 | - 4.4.2.1 接口说明
40 | - 4.4.2.2 查询用户是否在线
41 | - 4.4.2.3 发送消息
42 | - 4.4.2.4 给指定房间所有用户发送消息
43 | - 4.4.2.5 获取房间内全部用户
44 | - 5、webSocket项目Nginx配置
45 | - 5.1 为什么要配置Nginx
46 | - 5.2 nginx配置
47 | - 5.3 问题处理
48 | - 6、压测
49 | - 6.1 Linux内核优化
50 | - 6.2 压测准备
51 | - 6.3 压测数据
52 | - 7、如何基于webSocket实现一个分布式Im
53 | - 7.1 说明
54 | - 7.2 架构
55 | - 7.3 分布式系统部署
56 | - 8、回顾和反思
57 | - 8.1 在其它系统应用
58 | - 8.2 需要完善、优化
59 | - 8.3 总结
60 | - 9、参考文献
--------------------------------------------------------------------------------