├── .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 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/link1st/gowebsocket)](https://pkg.go.dev/github.com/link1st/gowebsocket/v2) 3 | [![Release](https://img.shields.io/github/v/release/link1st/gowebsocket)](https://github.com/link1st/gowebsocket/releases) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/link1st/gowebsocket/v2)](https://goreportcard.com/report/github.com/link1st/gowebsocket/v2) 5 | [![OpenIssue](https://img.shields.io/github/issues/link1st/gowebsocket)](https://github.com/link1st/gowebsocket/issues) 6 | [![ClosedIssue](https://img.shields.io/github/issues-closed/link1st/gowebsocket)](https://github.com/link1st/gowebsocket/issues?q=is%3Aissue+is%3Aclosed) 7 | ![Stars](https://img.shields.io/github/stars/link1st/gowebsocket) 8 | ![Forks](https://img.shields.io/github/forks/link1st/gowebsocket) 9 | [![Stargazers over time](https://starchart.cc/link1st/gowebsocket.svg?variant=adaptive)](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 | ![网站架构图](img/%E7%BD%91%E7%AB%99%E6%9E%B6%E6%9E%84%E5%9B%BE.png) 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 | ![HTTP协议和WebSocket比较](img/HTTP%E5%8D%8F%E8%AE%AE%E5%92%8CWebSocket%E6%AF%94%E8%BE%83.png) 104 | 105 | - HTTP和webSocket都支持配置证书,`ws://` 无证书 `wss://` 配置证书的协议标识 106 | ![HTTP协议和WebSocket比较](img/HTTP%E5%8D%8F%E8%AE%AE%E5%92%8CWebSocket%E6%AF%94%E8%BE%83.jpeg) 107 | 108 | ### 2.2 webSocket的兼容性 109 | - 浏览器的兼容性,开始支持webSocket的版本 110 | 111 | ![浏览器开始支持webSocket的版本](img/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%BC%80%E5%A7%8B%E6%94%AF%E6%8C%81webSocket%E7%9A%84%E7%89%88%E6%9C%AC.jpeg) 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 | ![服务端处理一个请求](img/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%A4%84%E7%90%86%E4%B8%80%E4%B8%AA%E8%AF%B7%E6%B1%82.jpeg) 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 | ![浏览器 Network](img/%E6%B5%8F%E8%A7%88%E5%99%A8%20Network.png) 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 | ![websocket接收和发送数据](img/websocket%E6%8E%A5%E6%94%B6%E5%92%8C%E5%8F%91%E9%80%81%E6%95%B0%E6%8D%AE.png) 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 | ![网站架构图](img/%E7%BD%91%E7%AB%99%E6%9E%B6%E6%9E%84%E5%9B%BE.png) 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 | ![用户连接时序图](img/%E7%94%A8%E6%88%B7%E8%BF%9E%E6%8E%A5%E6%97%B6%E5%BA%8F%E5%9B%BE.png) 1085 | 1086 | - 其它系统(IM、任务)向webSocket(acc)系统连接的用户发送消息时序图 1087 | 1088 | ![分布是系统随机给用户发送消息](img/%E5%88%86%E5%B8%83%E6%98%AF%E7%B3%BB%E7%BB%9F%E9%9A%8F%E6%9C%BA%E7%BB%99%E7%94%A8%E6%88%B7%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF.png) 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 | 添加link1st的微信 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 |
227 |
    228 |
229 | 230 | 234 |
235 |
236 |
237 |
238 | 管理员 239 |
240 |
241 | 242 | 欢迎加入聊天~ 243 |
244 |
245 |
246 |
247 | 249 |
250 | 251 | 252 |
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 |
239 |
    240 |
241 | 242 | 246 |
247 |
248 |
249 |
250 | 管理员 251 |
252 |
253 | 254 | 欢迎加入聊天~ 255 |
256 |
257 |
258 |
259 | 261 |
262 | 263 | 264 |
265 |
266 |
267 |
268 |
269 | 房间列表:
270 | 聊天室-ID:101 271 | 聊天室-ID:102 272 | 聊天室-ID:103 273 | 聊天室-ID:104 274 |
275 |
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、参考文献 --------------------------------------------------------------------------------