├── .gitignore ├── LICENSE ├── README.md ├── app.yaml ├── build_protoc.sh ├── common ├── define.go └── error_code.go ├── config └── config.go ├── docs ├── 性能优化.md └── 消息可靠性和有序性.md ├── go.mod ├── go.sum ├── lib ├── cache │ ├── group_cache.go │ ├── group_cache_test.go │ ├── seq_cache.go │ ├── seq_cache_test.go │ └── user_cache.go ├── etcd │ ├── discovery.go │ └── register.go └── mq │ ├── message.go │ └── message_test.go ├── main.go ├── model ├── friend.go ├── group.go ├── group_user.go ├── message.go ├── uid.go └── user.go ├── pkg ├── db │ ├── db.go │ ├── redis.go │ └── redis_test.go ├── etcd │ ├── etcd.go │ └── etcd_test.go ├── logger │ └── logger.go ├── middlewares │ └── auth.go ├── mq │ └── rabbitmq.go ├── protocol │ ├── pb │ │ ├── conn.pb.go │ │ ├── conn_grpc.pb.go │ │ ├── message.pb.go │ │ └── mq_msg.pb.go │ └── proto │ │ ├── conn.proto │ │ ├── message.proto │ │ └── mq_msg.proto ├── rpc │ └── client.go └── util │ ├── md5.go │ ├── panic.go │ ├── strconv.go │ ├── token.go │ ├── uid.go │ └── uid_test.go ├── profile ├── router ├── router.go └── ws_router.go ├── service ├── friend.go ├── group.go ├── group_user.go ├── rpc_server │ └── conn.go ├── seq.go ├── uid.go ├── user.go └── ws │ ├── conn.go │ ├── heartbeat.go │ ├── message.go │ ├── req.go │ └── server.go ├── sql └── create_table.sql └── test ├── router_test.go ├── ws_benchmark ├── client.go ├── main.go ├── manager.go └── timer.go └── ws_client └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 callmePicacho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoChat 2 | GoChat 是一款使用 Golang 实现的简易 IM 服务器,主要特性: 3 | 1. 支持 websocket 接入 4 | 2. 单聊、群聊 5 | 3. 离线消息同步 6 | 4. 支持服务水平扩展 7 | 8 | ## 技术栈 9 | - Web 框架:Gin 10 | - ORM 框架:GORM 11 | - 数据库:MySQL + Redis 12 | - 通讯框架:gRPC 13 | - 长连接通讯协议:Protocol Buffers 14 | - 日志框架:zap 15 | - 消息队列:RabbitMQ 16 | - 服务发现:ETCD 17 | - 配置管理:viper 18 | 19 | ## 架构 20 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/2518584/1681118536031-bbe50473-4e3f-42ee-a499-60bb9c41d484.png#averageHue=%23fbfbfb&clientId=ucf147b3b-fec6-4&from=paste&height=663&id=uc6f1af5e&name=image.png&originHeight=994&originWidth=1452&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=88159&status=done&style=none&taskId=u496cb626-bf43-4ed5-b223-cc95213b826&title=&width=968) 21 | 22 | ## 相关文档 23 | [消息可靠性和有序性](docs/消息可靠性和有序性.md) 24 | 25 | [性能优化](docs/性能优化.md) 26 | 27 | ## 项目启动 28 | docker 安装 MySQL、Redis、ETCD 和 RabbitMQ 29 | ```shell 30 | # ETCD 31 | docker run -d --name etcd -p 2379:2379 -p 2380:2380 -e ALLOW_NONE_AUTHENTICATION=yes -e ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 bitnami/etcd:latest 32 | # Redis 33 | docker run -d --name redis -p 6379:6379 redis 34 | # RabbitMQ 35 | docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management-alpine 36 | # MySQL 37 | docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql 38 | ``` 39 | 40 | 41 | 服务端启动: 42 | 1. 连接 MySQL,创建 gochat 库,进入执行 sql/create_table.sql 文件中 SQL 代码 43 | 2. app.yaml 修改配置文件信息 44 | 3. main.go 启动服务端 45 | 46 | 客户端启动: 47 | 1. 启动服务端后,执行 test/router_test.go 中测试可进行用户注册和群创建 48 | 2. test/ws_client/main.go 启动客户端 49 | 3. 启动多客户端可成功进行通讯 50 | 51 | 水平扩展: 52 | 1. 修改 app.yaml 中 `http_server_port`、`websocket_server_port` 和 `port`,启动第二个服务端 53 | 2. 修改 test/ws_client/main.go 中 `httpAddr` 和 `websocketAddr` 参数,启动第二个客户端 54 | 3. 连接不同服务端的客户端间亦可成功通讯 55 | 56 | ## 交互流程 57 | 58 | 建立 websocket 连接: 59 | 1. 客户端发送 HTTP 请求,但是携带升级协议的头部信息,路由:/ws 60 | 2. 服务端接收到升级协议的请求,创建 conn 对象,创建两个 goroutine,一个负责读,一个负责写,然后返回升级响应 61 | 3. 客户端收到响应信息,成功建立 websocket 连接 62 | ```text 63 | A Server 64 | - HTTP upgrade -> 65 | <- Response - 66 | ``` 67 | 68 | 客户端登录: 69 | 1. 客户端携带 `pb.Input{type: CT_Login, data: proto.Marshal(pb.LoginMsg{token:""})}` 进行登录 70 | 2. 服务端进行处理后,回复 ACK 71 | 1. 标记 userId 到 conn 的映射 72 | 2. Redis 记录 userId 发送消息所在 rpc 地址,跨节点连接能通过该 rpc 地址发送数据到其他节点 73 | 3. 将该 conn 加入到 connMap 进行心跳超时检测 74 | 4. 回复 ACK `pb.Output{type: CT_ACK, data: proto.Marshal(pb.ACKMsg{type: AT_Login})` 75 | 3. 客户端收到 ACK,暂时不进行处理 76 | 77 | ```text 78 | A Server 79 | - Login -> 80 | <- ACK - 81 | ``` 82 | 83 | 心跳和超时检测: 84 | 1. 客户端间隔时间携带 `pb.Input{type: CT_Heartbeat, data: proto.Marshal(pb.HeartbeatMsg{})}` 发送 85 | 2. 服务端收到心跳,啥也不干 86 | 3. 服务端维护的 conn 在每次读数据或写数据后会更新心跳时间,所以收到心跳消息,会更新 conn 活跃时间 87 | 4. 服务端定期进行超时检测,间隔时间获取全部连接信息,检测连接是否存活,及时清除超时连接 88 | 89 | ```text 90 | A Server 91 | - Heartbeat -> 92 | ``` 93 | 94 | 上行(客户端发送给服务端)消息投递: 95 | (上行消息依靠 clientId + ACK + 超时重传实现了消息可靠性和有序性,即:不丢、不重、有序) 96 | 1. 客户端发送消息,消息格式 `pb.Input{type: CT_Message, data: proto.Marshal(pb.UpMsg{ClientId: x, Msg:proto.Marshal(pb.Message{})}}` 97 | 2. 客户端每次发送消息,clientId++,并启动消息计时器,超时时间内未收到 ACK,再次重发消息 98 | 3. 服务端收到消息,处理后回复 ACK 99 | 1. 当且仅当 clientID = maxClientId+1,服务端接收此消息,并更新 maxClientId++ 100 | 2. 进行相应处理 101 | 3. 回复客户端 ACK `pb.Output{type: CT_ACK, data: proto.Marshal(pb.ACKMsg{type: AT_Up, ClientId: x, Seq: y})` 102 | 4. 客户端收到 ACK 后,取消超时重传,更新 seq(离线消息同步用到) 103 | 104 | ```text 105 | A Server 106 | - Message -> 107 | <- ACK - 108 | ``` 109 | 110 | 单聊、群聊消息处理: 111 | - 单聊消息处理:获取接收者id的 seq(单调递增),并将消息存入 Message 表,进行下行消息推送 112 | - 群聊使用写扩散,即当一个群成员发送消息,需要向所有群成员的消息列表插入一条记录(同上单聊) 113 | - 优点是每个用户只需要维护一个序列号(Seq)和消息列表,拉取离线消息时只需要拉取自己的消息列表即可获取全部消息 114 | - 缺点是每次转发时,群组有多少人,就需要插入多少条数据 115 | 116 | 117 | 下行消息投递: 118 | (考虑到性能问题,下行消息投递暂未实现消息可靠性和有序性) 119 | 下行消息涉及到一个问题:A 和 Server1 进行通信,投递消息给位于 Server2 的 B 该如何实现? 120 | 1. Server1 和 Server2 启动时,启动各自的 RPC 服务,当前 Server 通过调用其他 Server 的 RPC 方法,能将消息投递到其他 Server 121 | 2. Server1 处理完 A 发送的消息,组装出下行消息:`pb.Output{type: CT_Message, data: proto.Marshal(pb.PushMsg{Msg:proto.Marshal(pb.Message{})})` 122 | 3. 消息转发流程: 123 | 1. 根据 Redis 查询 userId 是否在线,如果不在线,不进行推送 124 | 2. 根据 connMap 查询是否在本地,如果在本地,进行本地推送 125 | 3. 如果不在本地,调用 RPC 方法 DeliverMessage 进行推送 126 | ```text 127 | A Server1 Server2 B 128 | - Message -> 129 | <- ACK - 130 | -- DeliverMessage > 131 | -- Message -> 132 | ``` 133 | 134 | 135 | 离线消息同步: 136 | 1. 客户端请求离线消息同步,消息格式:`pb.Input{type: CT_Sync, data: proto.Marshal(pb.SyncInputMsg{seq: x})}}}` 137 | 2. 服务端收到客户端请求,拉取该 userId 大于 seq 的消息列表前 n 条,返回:`pb.Output{type:CT_Sync, data: proto.Marshal(pb.SyncOutputMsg{Messages: "", hasMore: bool})}` 138 | 3. 客户端根据返回值 hasMore 是否为 true,更新本地 seq 后决定是否再次拉取消息 139 | ```text 140 | A Server 141 | - Sync -> 142 | <- 返回离线消息 - 143 | ``` 144 | 145 | 146 | ## 压测 147 | 148 | 名词解释: 149 | - PV(页面浏览量):用户每打开一个网站页面,记录一个 PV,用户多次打开同一页面,PV 值累计多次 150 | - UV(网站独立访客):通过互联网访问、流量网站的自然人。1天内相同访客多次访问网站,只计算为1个独立访客 151 | 152 | 压测指标: 153 | - 压测原则:每天 80% 的访问量集中在 20% 的时间内,这 20% 的时间就叫峰值 154 | - 公式:(总 PV * 80%)/ (86400 * 20%) = 峰值期间每秒请求数(QPS) 155 | - 峰值时间 QPS / 单台机器 QPS = 需要的机器数量 156 | - 举例:网站用户数 100W,每天约 3000W PV,单机承载,这台机器的性能需要多少 QPS? 157 | > (3000 0000 * 0.8) / (86400 * 0.2) ≈ 1398 QPS 158 | - 假设单机 QPS 为 70,需要多少台机器来支撑? 159 | > 1398 / 70 ≈ 20 160 | 161 | 使用 pprof 162 | 163 | 1. 引入 `github.com/gin-contrib/pprof` 164 | 2. 将路由注册进 pprof `pprof.Register(r)` 165 | 3. 启动服务 166 | 167 | 分析 CPU 耗时: 168 | 1. 访问 /debug/pprof 169 | 2. 访问 profile 等待 30s 可以得到一份 CPU profile 文件,得到性能数据,下面开始分析 170 | 3. 通过 `go tool pprof -http=":8081" ./profile` 进入 web 界面查看 CPU 使用情况 171 | 4. 左上角的 VIEW 中: 172 | - Top 按程序运行时间 flat 排序的函数列表 173 | - Graph 是连线图,越大越红的块耗时越多 174 | - Flame Graph 是火焰图,越大块的函数耗时越多 175 | - Peek 同 top 列表,打印每个函数的调用栈 176 | - Source 按程序运行耗时展示函数内部具体耗时情况 177 | 内存情况: 178 | 179 | 分析其他(内存、goroutine、mutex、block)情况: 180 | 1. 直接通过 `go tool pprof -http=":8081" http://localhost:9091/debug/pprof/heap` 进入 web 页面查看内存使用情况,其他指标同理 181 | 182 | 本机压测结果: 183 | ```text 184 | /* 185 | ------ 目标:单机群聊压测 ------ 186 | 注:本机压本机 187 | 系统:Windows 10 19045.2604 188 | CPU: 2.90 GHz AMD Ryzen 7 4800H with Radeon Graphics 189 | 内存: 16.0 GB 190 | 群成员总数: 500人 191 | 在线人数: 500人 192 | 每秒/次发送消息数量: 500条 193 | 每秒理论响应消息数量:25 0000条 = 500条 * 在线500人 194 | 发送消息次数: 40次 195 | 响应消息总量:1000 0000条 = 500条 * 在线500人 * 40次 196 | Message 表数量总数:1000 0000条 = 总数500人 * 500条 * 40次 197 | 丢失消息数量: 0条 198 | 总耗时: 39 948ms(39s) 199 | 平均每500条消息发送/转发在线人员/在线人员接收总耗时: 998ms(其实更短,因为消息是每秒发一次) 200 | 201 | 如果发送消息次数为 1,时间为:940ms 202 | */ 203 | ``` 204 | 205 | 内存占用: 206 | 经过测试 1w 连接数,消耗内存:300M 207 | 均约内存占用 30KB/连接,支持百万连接需要 30G 内存,理论上单机是可以实现的,优化方案可以采用 I/O 多路复用减少 goroutine 数 208 | 209 | ## TODO 210 | 1. 接入层尝试实现主从 Reactor 线程模型后,再进行性能测试(参考:https://github.com/eranyanay/1m-go-websockets.git) 211 | 2. ~~更友好的日志~~ 212 | 3. 增加负载均衡,选择合适的 WebSocket 服务 213 | 4. 实现下行消息可靠性,使用时间轮(参考:https://github.com/aceld/zinx/blob/HEAD/ztimer/timewheel.go) 214 | 5. 实现 docker-compose 脚本 215 | 6. 完善客户端 sdk,实现 GUI 216 | 7. prometheus 系统监控 217 | 8. ~~递增 id 使用微信消息序列号生成的思路,使用双 buffer(参考:http://www.52im.net/thread-1998-1-1.html)~~ 218 | 9. 优化完善,实现更多功能 -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | jwt: 2 | sign_key: "gooooIM" 3 | expire_time: 720 # hour 4 | mysql: 5 | dns: "root:root@tcp(127.0.0.1:3306)/gochat?charset=utf8mb4&parseTime=True&loc=Local" 6 | redis: 7 | addr: "127.0.0.1:6379" 8 | password: "" 9 | app: 10 | salt: "gogogoChat" # 密码加盐 11 | ip: "127.0.0.1" 12 | http_server_port: "9090" # http 端口 13 | websocket_server_port: "9091" # websocket 端口 14 | rpc-port: "9092" # rpc 端口 15 | worker_pool_size: 10 # 业务 worker 队列数量 16 | max_worker_task: 1024 # 业务 worker 队列中,每个 worker 的最大任务存储数量 17 | heartbeattime: 600 # 心跳超时时间 s ,10 * 60 18 | heartbeatInterval: 60 # 超时连接检测间隔 s 19 | etcd: 20 | endpoints: # etcd 端口列表 21 | - "localhost:2379" 22 | timeout: 5 # 超时时间 s 23 | rabbitmq: 24 | url: "amqp://guest:guest@localhost:5672/" 25 | log: 26 | target: "file" # 日志输出路径:可选值 console/file 27 | level: "debug" # 日志输出级别 debug、info、warn、error、dpanic、panic、fatal -------------------------------------------------------------------------------- /build_protoc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | root_path=$(pwd) 6 | rm -rf pkg/protocol/pb/* 7 | cd pkg/protocol/proto 8 | pb_root_path=$root_path/../ 9 | protoc --proto_path=$root_path/pkg/protocol/proto --go_out=$pb_root_path --go-grpc_out=$pb_root_path *.proto -------------------------------------------------------------------------------- /common/define.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | // EtcdServerList ETCD服务列表路径 5 | EtcdServerList = "/wsServers/" 6 | ) 7 | -------------------------------------------------------------------------------- /common/error_code.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | OK = 200 // Success 5 | NotLoggedIn = 1000 // 未登录 6 | ParameterIllegal = 1001 // 参数不合法 7 | UnauthorizedUserId = 1002 // 非法的用户Id 8 | Unauthorized = 1003 // 未授权 9 | ServerError = 1004 // 系统错误 10 | NotData = 1005 // 没有数据 11 | ModelAddError = 1006 // 添加错误 12 | ModelDeleteError = 1007 // 删除错误 13 | ModelStoreError = 1008 // 存储错误 14 | OperationFailure = 1009 // 操作失败 15 | RoutingNotExist = 1010 // 路由不存在 16 | ) 17 | 18 | var codeMap = map[uint32]string{ 19 | OK: "Success", 20 | NotLoggedIn: "未登录", 21 | ParameterIllegal: "参数不合法", 22 | UnauthorizedUserId: "非法的用户Id", 23 | Unauthorized: "未授权", 24 | NotData: "没有数据", 25 | ServerError: "系统错误", 26 | ModelAddError: "添加错误", 27 | ModelDeleteError: "删除错误", 28 | ModelStoreError: "存储错误", 29 | OperationFailure: "操作失败", 30 | RoutingNotExist: "路由不存在", 31 | } 32 | 33 | // GetErrorMessage 根据错误码 获取错误信息 34 | func GetErrorMessage(code uint32, message string) string { 35 | codeMessage := message 36 | if message == "" { 37 | if value, ok := codeMap[code]; ok { 38 | // 存在 39 | codeMessage = value 40 | } else { 41 | codeMessage = "未定义错误类型!" 42 | } 43 | } 44 | 45 | return codeMessage 46 | } 47 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "GoChat/pkg/logger" 5 | "fmt" 6 | "github.com/spf13/viper" 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | var GlobalConfig *Configuration 12 | 13 | type Configuration struct { 14 | // JWT 配置 15 | JWT struct { 16 | SignKey string `mapstructure:"sign_key"` // JWT 签名密钥 17 | ExpireTime int `mapstructure:"expire_time"` // JWT 过期时间(小时) 18 | } `mapstructure:"jwt"` 19 | 20 | // MySQL 配置 21 | MySQL struct { 22 | DNS string `mapstructure:"dns"` // 数据库连接字符串 23 | } `mapstructure:"mysql"` 24 | 25 | // Redis 配置 26 | Redis struct { 27 | Addr string `mapstructure:"addr"` // Redis 地址 28 | Password string `mapstructure:"password"` // Redis 认证密码 29 | } `mapstructure:"redis"` 30 | 31 | // 应用程序配置 32 | App struct { 33 | Salt string `mapstructure:"salt"` // 密码加盐 34 | IP string `mapstructure:"ip"` // 应用程序 IP 地址 35 | HTTPServerPort string `mapstructure:"http_server_port"` // HTTP 服务器端口 36 | WebsocketPort string `mapstructure:"websocket_server_port"` // WebSocket 服务器端口 37 | RPCPort string `mapstructure:"rpc-port"` // RPC 服务器端口 38 | WorkerPoolSize uint32 `mapstructure:"worker_pool_size"` // 业务 worker 队列数量 39 | MaxWorkerTask int `mapstructure:"max_worker_task"` // 业务 worker 对应负责的任务队列最大任务存储数量 40 | HeartbeatTimeout int `mapstructure:"heartbeattime"` // 心跳超时时间(秒) 41 | HeartbeatInterval int `mapstructure:"heartbeatInterval"` // 超时连接检测间隔(秒) 42 | } `mapstructure:"app"` 43 | 44 | // ETCD相关配置 45 | ETCD struct { 46 | Endpoints []string `mapstructure:"endpoints"` // etcd endpoints 列表 47 | Timeout int `mapstructure:"timeout"` // 超时时间(秒) 48 | } `mapstructure:"etcd"` 49 | 50 | RabbitMQ struct { 51 | URL string `mapstructure:"url"` // rabbitmq url 52 | } 53 | 54 | Log struct { 55 | Target string `mapstructure:"target"` // 日志输出路径:可选值 console/file 56 | Level string `mapstructure:"level"` // 日志输出级别 57 | LevelNum zapcore.Level 58 | } 59 | } 60 | 61 | func (c Configuration) String() string { 62 | return fmt.Sprintf( 63 | "JWT:\n SignKey: %s\n ExpireTime: %d\nMySQL:\n DNS: %s\nRedis:\n Addr: %s\n Password: %s\nApp:\n Salt: %s\n IP: %s\n HTTPServerPort: %s\n WebsocketPort: %s\n RPCPort: %s\n WorkerPoolSize: %d\n MaxWorkerTask: %d\n HeartbeatTimeout: %d\n HeartbeatInterval: %d\nETCD:\n Endpoints: %v\n Timeout: %d\nRabbitMQ:\n URL: %s\nLog:\n Target: %s\n Level: %s\n", 64 | c.JWT.SignKey, 65 | c.JWT.ExpireTime, 66 | c.MySQL.DNS, 67 | c.Redis.Addr, 68 | c.Redis.Password, 69 | c.App.Salt, 70 | c.App.IP, 71 | c.App.HTTPServerPort, 72 | c.App.WebsocketPort, 73 | c.App.RPCPort, 74 | c.App.WorkerPoolSize, 75 | c.App.MaxWorkerTask, 76 | c.App.HeartbeatTimeout, 77 | c.App.HeartbeatInterval, 78 | c.ETCD.Endpoints, 79 | c.ETCD.Timeout, 80 | c.RabbitMQ.URL, 81 | c.Log.Target, 82 | c.Log.Level, 83 | ) 84 | } 85 | 86 | func InitConfig(configPath string) { 87 | viper.SetConfigFile(configPath) 88 | err := viper.ReadInConfig() 89 | if err != nil { 90 | fmt.Println(err) 91 | } 92 | 93 | GlobalConfig = new(Configuration) 94 | err = viper.Unmarshal(GlobalConfig) 95 | if err != nil { 96 | panic(fmt.Errorf("unable to decode into struct, %v", err)) 97 | } 98 | reload() 99 | 100 | // 初始化 log 101 | logger.InitLogger(GlobalConfig.Log.Target, GlobalConfig.Log.LevelNum) 102 | logger.Logger.Debug("config init ok", zap.String("GlobalConfig", GlobalConfig.String())) 103 | } 104 | 105 | func reload() { 106 | // 最小为 10 107 | if GlobalConfig.App.WorkerPoolSize < 10 { 108 | GlobalConfig.App.WorkerPoolSize = 10 109 | } 110 | // 最小为 1024 111 | if GlobalConfig.App.MaxWorkerTask < 1000 { 112 | GlobalConfig.App.MaxWorkerTask = 1024 113 | } 114 | // 默认为控制台 115 | if GlobalConfig.Log.Target == "file" { 116 | GlobalConfig.Log.Target = logger.File 117 | } else { 118 | GlobalConfig.Log.Target = logger.Console 119 | } 120 | // 如果解析失败默认为 Error 级别 121 | var err error 122 | GlobalConfig.Log.LevelNum, err = zapcore.ParseLevel(GlobalConfig.Log.Level) 123 | if err != nil { 124 | GlobalConfig.Log.LevelNum = zapcore.ErrorLevel 125 | } 126 | fmt.Println("日志级别为:", GlobalConfig.Log.LevelNum) 127 | fmt.Println("日志输出到:", GlobalConfig.Log.Target) 128 | } 129 | -------------------------------------------------------------------------------- /docs/性能优化.md: -------------------------------------------------------------------------------- 1 | 单次的定义:
本机启动 ETCD、Redis、MySQL 和 RabbitMQ
服务端和客户端都在本地
群成员总数:500 人
在线人数:300 人
每秒发送消息数量:100次
发送消息次数:1次
响应消息数量:3 0000次
丢失消息数量:0条
Message 表数量总数:5 0000条
总耗时:3.5s -> 0.2s
优化目的:降低消息推送延迟 2 | 3 | ### 服务端执行流程 4 | 群聊为例:
![image.png](https://cdn.nlark.com/yuque/0/2023/png/2518584/1681127942305-70edc6f4-8b61-4a40-968e-bd4f97f761ac.png#averageHue=%23fafafa&clientId=u3911ca04-3ca5-4&from=paste&height=606&id=ue5664aba&name=image.png&originHeight=909&originWidth=1311&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=118667&status=done&style=none&taskId=u269aabf1-ee75-4f64-9132-b41e2908f9e&title=&width=874) 5 | 6 | 1. Server 启动时,启动 worker pool 7 | 2. Client 请求建立连接,Server 为其创建 read 和 write 协程 8 | 3. Client 发送消息,read 读取并解析消息,根据消息类型赋予不同的路由函数,发送给 worker pool 等待调度业务层执行 9 | 4. 业务层执行实际路由消息,如果是群聊消息发送: 10 | 1. 给自己发送一条消息(获取 seqId 和落库 Message 记录,但是不进行推送) 11 | 2. 根据 groupId 从 MySQL 中获取群成员信息 12 | 3. 验证发送者是否属于群聊 13 | 4. 对于每个除发送者之外的群成员: 14 | 1. 从 MySQL 获取该用户的 seqId(select seq where userId = ? and object_id = ? for update) 15 | 2. 消息携带刚刚获取的 seqId 落库 16 | 3. 组装下行消息进行推送 17 | 4. 查询用户是否在线(用户通过 websocket 进行 Login 时,接入层本地存储 userId:conn 的映射,Redis 存储该 userId:RPC address 的映射),如果用户不在线,返回 18 | 5. 查询是否用户的长连接是否在本地,如果在本地,直接进行本地推送 19 | 6. RPC 服务提供接入层消息投递接口,通过 Redis 中 userId 映射的 RPC addr 获取到 gRPC 连接后调用 RPC 方法进行消息投递 20 | 21 | ### 思路1:缓存 22 | 应用缓存带来的问题: 23 | 24 | 1. 缓存自身的问题 25 | 2. 数据一致性和可用性问题 26 | 27 | 1. 创建群聊时,将群组信息存入 Redis(群组相关功能:增、删、改) 28 | 2. user 的 seqId 使用 Redis incr(后续优化可以使用预分配中间层,思想是直接使用内存作为缓存) 29 | 30 | ### 思路2:批处理 31 | 32 | 1. 集中获取用户的 seqId,为了保证顺序性,使用 lua 脚本,批次进行处理,一次最多执行 1k 个用户的 incr 脚本 33 | 2. 批量消息落库,每次落库 500 个对象 34 | 3. 消息下发时,之前都是先查用户是否在线,在哪个网关地址,再单独投递。群聊场景下,直接将全部消息投递给所有长连接网关,让它本地查找哪些用户在线,在线就进行推送,需要引入 ETCD 做服务注册 35 | 4. 消息收发过于频繁,发送消息时,暂存 buffer,当 buffer 数量满足或者到间隔时间时间,打包发送 buffer 中的数据,提高整体吞吐但是单条消息延迟上升 36 | 37 | 38 | ### 思路3:异步处理 39 | 异步处理带来的问题:系统复杂度上升 40 | 41 | 1. 消息推送不必等到消息落库后再进行,消息落库可以异步,引入 MQ 来做 42 | 2. 消息推送是推送给不同客户端,可以异步处理,但是需要限制并发数量,比如 5 个 43 | 44 | 45 | ### 思路4:优化数据结构 46 | 带来的问题:系统复杂性上升 47 | 48 | 1. 场景本身:读多写少 map+mutex VS sync.Map(读多写少场景性能好) VS concurrent-map(分段锁) 49 | 2. json -> proto 10倍性能提升 50 | 3. 推送后使用时间轮替代原来的 time.NewTicker(四叉树),增删 O(LogN) -> 增删 O(1),损失精度 51 | 4. 接入层 I/O 多路复用(其实已经算优化系统架构了) 52 | 53 | 54 | ### 思路5:池化 55 | 56 | 1. 协程池 57 | 2. 连接池 58 | 59 | 60 | 参考: 61 | 62 | 1. [企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等](http://www.52im.net/thread-3631-1-1.html) 63 | 2. [从3s到25ms!看看人家的接口优化技巧,确实很优雅!](https://mp.weixin.qq.com/s/oMStgpD_5vFsBEt-Huq8zQ) 64 | 3. [zinx时间轮实现](https://github.com/aceld/zinx/blob/3d5c30bf15f00cf7b668115d118aec0dcdd5294e/ztimer/timerscheduler.go) 65 | 4. [1m-go-websockets](https://github.com/eranyanay/1m-go-websockets) -------------------------------------------------------------------------------- /docs/消息可靠性和有序性.md: -------------------------------------------------------------------------------- 1 | 2 | ### 定义 3 | 消息可靠性:不丢失、不重复
消息有序性:任意时刻消息保证与发送端顺序一致
总结:不丢、不重、有序 4 | 5 | ### 典型的 IM 架构 6 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/2518584/1681025692120-760483f2-a3f0-40cc-b2f0-0b23b311665b.png#averageHue=%23e3e7f6&clientId=u04238ca7-f355-4&from=paste&height=227&id=ua5dc6379&name=image.png&originHeight=341&originWidth=777&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=72137&status=done&style=none&taskId=uc70a64e5-8fae-4473-a064-31dc7017100&title=&width=518) 7 |
典型的服务端中转型IM架构:一条消息从 clientA 发出后,需要先经过 IM 服务器来进行中转,然后再由 IM 服务器推送给 clientB
所以准确来说:
消息的可靠性 = 上行消息可靠 + 服务端消息可靠 + 下行消息可靠
消息的有序性 = 上行消息有序 + 服务端消息有序 + 下行消息有序 8 | 9 | ### TCP 并不能保证消息的可靠性和有序性 10 | TCP 是网络层的协议,只能保证网络层间的可靠传输和数据有序,并不能保障应用层的可靠性和有序性 11 | 12 | - clientA 发送的数据可靠抵达 Server 网络层后,还需要应用层进行处理,此时 Server 进程崩溃后重启,clientA 认为已经送达,但是 Server 业务层无感知,因此**消息丢失** 13 | - clientA 发送 msg1 和 msg2 达到应用层,解析后交给两个线程处理,msg2 先落库,造成**消息乱序** 14 | 15 | ### 如何保障消息的可靠性 16 | TCP 虽然不能直接帮我们,但是我们可以借鉴 TCP 的可靠传输:超时重传 + 消息确认(ACK) + 消息去重,我们可以实现应用层的消息确认机制 17 | 18 | 通过在应用层加入超时重传 + 消息确认机制,保障了消息不会丢失,但是带来了新问题:消息重复,TCP 其实也告诉我们答案了,消息id,幂等去重 19 | 20 | ### 如何保证消息的有序性 21 | 保证消息有序性的难点在于:没有全局时钟 22 |
缩小思路:其实不需要全局序列,在会话范围(单聊、群聊)内有序即可 23 |
解决思路:仿微信的序列号生成思路,将标识消息唯一性的 id 和标识消息有序性的 id 单独拆开 24 |
现在我们只需要考虑该 id: 25 | 26 | 1. 会话内唯一 27 | 2. 单调递增 28 | 29 | 简单实现:对于每个用户,使用 Redis incr 命令得到递增的序号;Redis 可能挂,换另一个节点可能导致 30 | 31 | 优化点:递增 id 使用微信消息序列号生成的思路,使用双 buffer 32 | 33 | ### 项目实现 34 | 35 | #### 上行消息的可靠性 36 | clientA -> Server:使用 clientId 保证 37 | 38 | 1. clientA 创建连接后,初始化 clientId = 0,发送消息时携带 clientId,并且 clientId++ 39 | 2. clientA 发送消息后创建消息计时器,存入以 clienId 为 key,以 context.WithCancel 返回的 cancel 函数为 value 的 map,有限次数内指定间隔后 (利用 time.Ticker)重发消息,或者收到该 clientId 的 ACK 回复 40 | 3. Server 收到消息,解析后得到消息中的 clientId,Server 中维护当前连接收到的最大 max_clientId,当且仅当 max_clientId+1 == clientId,才接收该消息,否则拒绝 41 | 4. 仅当 Server 收到消息后,经过处理,回复 clientA 携带 clientId 的 ACK 消息 42 | 5. clientA 收到 ACK 消息后,根据 clientId 获取 cancel 函数执行 43 | 44 | 缺点:依靠 clientId 只能保证发送方的消息顺序,无法保证整个会话中消息的有序性
会话消息的有序性需要服务端的 id 来保证 45 | 46 | #### 服务端消息的可靠性 47 | 消息在 Server 中处理时的可靠性:使用 MQ + seqId 保证
48 | 49 | 以 userid 作为 key,使用 Redis incr 递增生成 seqId 50 | 51 | 1. 消息到达 Server,Server 根据 max_clientId + 1 == clientId 校验是否接收消息,如果接收消息,更新 max_clientId 为 clientId,然后继续往下执行 52 | 2. Server 请求 Redis 获取发送者 userid incr 得到新的 seqId,并落库消息 53 | 3. Server 将消息写入 MQ,交给 MQ 的消费者异步处理,MQ 保证服务端消息可靠性 54 | 4. Server 回复 clientA ACK 消息,携带接收消息中的 clientId 和前面发送者得到的最新 seqId 55 | 5. Server 中的 MQ 处理消息前,通过 Redis 获取收件人 userId 的 seqId,落库消息,并进行下行消息推送 56 | 6. Server 发送消息后创建消息计时器,丢入时间轮等待超时重发,或者收到 clientB ACK 后取消超时重发 57 | 58 | 群聊消息通过 seqId 保证: 59 | 60 | - 单个客户端群聊中,看到的任一(任何一个)客户端消息顺序和其发送消息顺序一致(群聊中存在 ABC,A 看到 B 的消息肯定和 B 的发送顺序一致,A 看到 C 的消息肯定和 C 的发送顺序一致) 61 | - 在多个客户端参与同一个群聊时,每个客户端所看到的来自任何一个客户端发送的消息以及消息发送的顺序都是一致的。但是,不同客户端所看到的消息顺序可能不同(群聊中存在 ABC,A 看到 B 的消息肯定和 B 的发送顺序一致,C 看到 B 的消息也肯定和 B 的发送顺序一致,但是 A 看到的整体消息可能和 C 看到的整体消息顺序不一致) 62 | 63 | 优化点: 64 | 65 | 1. 使用会话层面的 id,能保证群聊绝对有序,但是需要再构建会话层,维护更多状态,不确定是否值得 66 | 2. 换一种 id 生成的方式 67 | 68 | #### 下行消息的可靠性 69 | Server -> clientB:使用 seqId 保证 70 | 71 | 1. Server 携带该用户最新 seqId 发送消息给 clientB 72 | 2. clientB 检查消息中的 seqId 是否等于自己本地存的 seqId+1,如果是则直接显示,回复 Server ACK 消息并更新本地 seqId 73 | 3. 如果不是(seqId 不等于本地存的 seqId +1),则携带最新消息中的 seqId 进行离线消息同步 74 | 75 | 离线消息同步: 76 | 77 | 1. 客户端登录时,携带本地存储的 seqId 拉取离线消息 78 | 2. 服务端分页返回所有当前用户消息表中大于 seqId 的消息(WHERE userId = x AND seqId > x LIMIT n ORDER BY seq ASC) 79 | 3. 客户端收到离线数据,并根据返回参数检查是否还需要继续拉取数据 80 | 81 | 82 | 83 | 84 | ### 参考资料 85 | 86 | 1. [零基础IM开发入门(三):什么是IM系统的可靠性?](http://www.52im.net/thread-3182-1-1.html) 87 | 2. [零基础IM开发入门(四):什么是IM系统的消息时序一致性?](http://www.52im.net/thread-3189-1-1.html) 88 | 3. [IM消息送达保证机制实现(一):保证在线实时消息的可靠投递](http://www.52im.net/thread-294-1-1.html) 89 | 4. [理解IM消息“可靠性”和“一致性”问题,以及解决方案探讨](http://www.52im.net/thread-3574-1-1.html) 90 | 5. [消息协议设计(二)-消息可用性](https://hardcore.feishu.cn/docs/doccnGAMamrsjNx8g5BeptUiURd#T4Sqa8) 91 | 6. [从0到1再到N,探索亿级流量的IM架构演绎](https://nxwz51a5wp.feishu.cn/docs/doccnTYWSZg4v9bYTQH8hXkGJPc#wlfyuS) 92 | 7. [一个低成本确保IM消息时序的方法探讨](http://www.52im.net/thread-866-1-1.html) 93 | 8. [Leaf——美团点评分布式ID生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html) 94 | 9. [IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)](http://www.52im.net/thread-3129-1-1.html) 95 | 10. [IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)](http://www.52im.net/thread-1998-1-1.html) 96 | 11. https://github.com/alberliu/gim -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module GoChat 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.0 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/golang-jwt/jwt/v4 v4.5.0 9 | github.com/gorilla/websocket v1.5.0 10 | github.com/spf13/viper v1.15.0 11 | github.com/wagslane/go-rabbitmq v0.12.3 12 | go.etcd.io/etcd/api/v3 v3.5.7 13 | go.etcd.io/etcd/client/v3 v3.5.7 14 | google.golang.org/grpc v1.52.0 15 | google.golang.org/protobuf v1.28.1 16 | gorm.io/driver/mysql v1.4.7 17 | gorm.io/gorm v1.24.6 18 | ) 19 | 20 | require ( 21 | github.com/bytedance/sonic v1.8.0 // indirect 22 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 24 | github.com/coreos/go-semver v0.3.0 // indirect 25 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 26 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 27 | github.com/fsnotify/fsnotify v1.6.0 // indirect 28 | github.com/gin-contrib/pprof v1.4.0 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.11.2 // indirect 33 | github.com/go-sql-driver/mysql v1.7.0 // indirect 34 | github.com/goccy/go-json v0.10.0 // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/golang/protobuf v1.5.2 // indirect 37 | github.com/hashicorp/hcl v1.0.0 // indirect 38 | github.com/jinzhu/inflection v1.0.0 // indirect 39 | github.com/jinzhu/now v1.1.5 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 42 | github.com/leodido/go-urn v1.2.1 // indirect 43 | github.com/magiconair/properties v1.8.7 // indirect 44 | github.com/mattn/go-isatty v0.0.17 // indirect 45 | github.com/mitchellh/mapstructure v1.5.0 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 49 | github.com/rabbitmq/amqp091-go v1.8.0 // indirect 50 | github.com/spf13/afero v1.9.3 // indirect 51 | github.com/spf13/cast v1.5.0 // indirect 52 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/subosito/gotenv v1.4.2 // indirect 55 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 56 | github.com/ugorji/go/codec v1.2.9 // indirect 57 | go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect 58 | go.uber.org/atomic v1.9.0 // indirect 59 | go.uber.org/multierr v1.8.0 // indirect 60 | go.uber.org/zap v1.24.0 61 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 62 | golang.org/x/crypto v0.5.0 // indirect 63 | golang.org/x/net v0.7.0 // indirect 64 | golang.org/x/sys v0.5.0 // indirect 65 | golang.org/x/text v0.7.0 // indirect 66 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect 67 | gopkg.in/ini.v1 v1.67.0 // indirect 68 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 69 | gopkg.in/yaml.v3 v3.0.1 // indirect 70 | moul.io/zapgorm2 v1.3.0 71 | ) 72 | -------------------------------------------------------------------------------- /lib/cache/group_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "GoChat/pkg/util" 6 | "context" 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | const ( 12 | groupUserPrefix = "group_user_" // 群成员信息 13 | ttl2H = 2 * 60 * 60 // 2h 14 | ) 15 | 16 | func getGroupUserKey(groupId uint64) string { 17 | return fmt.Sprintf("%s%d", groupUserPrefix, groupId) 18 | } 19 | 20 | // SetGroupUser 设置群成员 21 | func SetGroupUser(groupId uint64, userIds []uint64) error { 22 | key := getGroupUserKey(groupId) 23 | values := make([]string, 0, len(userIds)) 24 | for _, userId := range userIds { 25 | values = append(values, util.Uint64ToStr(userId)) 26 | } 27 | _, err := db.RDB.SAdd(context.Background(), key, values).Result() 28 | if err != nil { 29 | fmt.Println("[设置群成员信息] 错误,err:", err) 30 | return err 31 | } 32 | _, err = db.RDB.Expire(context.Background(), key, ttl2H*time.Second).Result() 33 | if err != nil { 34 | fmt.Println("[设置群成员信息] 过期时间设置错误,err:", err) 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | // GetGroupUser 获取群成员 41 | func GetGroupUser(groupId uint64) ([]uint64, error) { 42 | key := getGroupUserKey(groupId) 43 | result, err := db.RDB.SMembers(context.Background(), key).Result() 44 | if err != nil { 45 | fmt.Println("[获取群成员信息] 错误,err:", err) 46 | return nil, err 47 | } 48 | userIds := make([]uint64, 0, len(result)) 49 | for _, v := range result { 50 | userIds = append(userIds, util.StrToUint64(v)) 51 | } 52 | return userIds, nil 53 | } 54 | -------------------------------------------------------------------------------- /lib/cache/group_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/pkg/db" 6 | "testing" 7 | ) 8 | 9 | func TestGroupUser(t *testing.T) { 10 | config.InitConfig("../../app.yaml") 11 | db.InitRedis(config.GlobalConfig.Redis.Addr, config.GlobalConfig.Redis.Password) 12 | 13 | var groupId uint64 = 77777 14 | _, err := GetGroupUser(groupId) 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/cache/seq_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "context" 6 | "fmt" 7 | "github.com/go-redis/redis/v8" 8 | ) 9 | 10 | const ( 11 | seqPrefix = "object_seq_" // 群成员信息 12 | 13 | SeqObjectTypeUser = 1 // 用户 14 | ) 15 | 16 | // 消息同步序列号 17 | func getSeqKey(objectType int8, objectId uint64) string { 18 | return fmt.Sprintf("%s%d_%d", seqPrefix, objectType, objectId) 19 | } 20 | 21 | // GetNextSeqId 获取用户的下一个 seq,消息同步序列号 22 | func GetNextSeqId(objectType int8, objectId uint64) (uint64, error) { 23 | key := getSeqKey(objectType, objectId) 24 | result, err := db.RDB.Incr(context.Background(), key).Uint64() 25 | if err != nil { 26 | fmt.Println("[获取seq] 失败,err:", err) 27 | return 0, err 28 | } 29 | return result, nil 30 | } 31 | 32 | // GetNextSeqIds 获取多个对象的下一个 seq,消息同步序列号 33 | func GetNextSeqIds(objectType int8, objectIds []uint64) ([]uint64, error) { 34 | script := ` 35 | local results = {} 36 | for i, key in ipairs(KEYS) do 37 | results[i] = redis.call('INCR', key) 38 | end 39 | return results 40 | ` 41 | keys := make([]string, len(objectIds)) 42 | for i, objectId := range objectIds { 43 | keys[i] = getSeqKey(objectType, objectId) 44 | } 45 | res, err := redis.NewScript(script).Run(context.Background(), db.RDB, keys).Result() 46 | if err != nil { 47 | fmt.Println("[获取seq] 失败,err:", err) 48 | return nil, err 49 | } 50 | results := make([]uint64, len(objectIds)) 51 | for i, v := range res.([]interface{}) { 52 | results[i] = uint64(v.(int64)) 53 | } 54 | return results, nil 55 | } 56 | -------------------------------------------------------------------------------- /lib/cache/seq_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/pkg/db" 6 | "testing" 7 | ) 8 | 9 | func TestGetNextSeqIds(t *testing.T) { 10 | config.InitConfig("../../app.yaml") 11 | db.InitRedis(config.GlobalConfig.Redis.Addr, config.GlobalConfig.Redis.Password) 12 | 13 | userIds := []uint64{1, 2, 3, 4, 5} 14 | ids, err := GetNextSeqIds(SeqObjectTypeUser, userIds) 15 | if err != nil { 16 | panic(err) 17 | } 18 | t.Log(ids) 19 | } 20 | -------------------------------------------------------------------------------- /lib/cache/user_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "context" 6 | "fmt" 7 | "github.com/go-redis/redis/v8" 8 | "time" 9 | ) 10 | 11 | const ( 12 | userOnlinePrefix = "user_online_" // 用户在线状态设置 13 | ttl1D = 24 * 60 * 60 // s 1天 14 | ) 15 | 16 | func getUserKey(userId uint64) string { 17 | return fmt.Sprintf("%s%d", userOnlinePrefix, userId) 18 | } 19 | 20 | // SetUserOnline 设置用户在线 21 | func SetUserOnline(userId uint64, addr string) error { 22 | key := getUserKey(userId) 23 | _, err := db.RDB.Set(context.Background(), key, addr, ttl1D*time.Second).Result() 24 | if err != nil { 25 | fmt.Println("[设置用户在线] 错误, err:", err) 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | // GetUserOnline 获取用户在线地址 32 | // 如果获取不到,返回 addr = "" 且 err 为 nil 33 | func GetUserOnline(userId uint64) (string, error) { 34 | key := getUserKey(userId) 35 | addr, err := db.RDB.Get(context.Background(), key).Result() 36 | if err != nil && err != redis.Nil { 37 | fmt.Println("[获取用户在线] 错误,err:", err) 38 | return "", err 39 | } 40 | return addr, nil 41 | } 42 | 43 | // DelUserOnline 删除用户在线信息(存在即在线) 44 | func DelUserOnline(userId uint64) error { 45 | key := getUserKey(userId) 46 | _, err := db.RDB.Del(context.Background(), key).Result() 47 | if err != nil { 48 | fmt.Println("[删除用户在线] 错误, err:", err) 49 | return err 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /lib/etcd/discovery.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "GoChat/config" 5 | "context" 6 | "fmt" 7 | "go.etcd.io/etcd/api/v3/mvccpb" 8 | clientV3 "go.etcd.io/etcd/client/v3" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Discovery 服务发现 14 | type Discovery struct { 15 | client *clientV3.Client // etcd client 16 | serverMap sync.Map 17 | } 18 | 19 | func NewDiscovery() (*Discovery, error) { 20 | client, err := clientV3.New(clientV3.Config{ 21 | Endpoints: config.GlobalConfig.ETCD.Endpoints, 22 | DialTimeout: time.Duration(config.GlobalConfig.ETCD.Timeout) * time.Second, 23 | }) 24 | if err != nil { 25 | fmt.Println("etcd err:", err) 26 | return nil, err 27 | } 28 | return &Discovery{client: client}, nil 29 | } 30 | 31 | // WatchService 初始化服务列表和监视 32 | func (d *Discovery) WatchService(prefix string) error { 33 | //根据前缀获取现有的key 34 | resp, err := d.client.Get(context.TODO(), prefix, clientV3.WithPrefix()) 35 | if err != nil { 36 | return err 37 | } 38 | for i := range resp.Kvs { 39 | if v := resp.Kvs[i]; v != nil { 40 | d.serverMap.Store(string(resp.Kvs[i].Key), string(resp.Kvs[i].Value)) 41 | } 42 | } 43 | d.watcher(prefix) 44 | // 监听前缀 45 | return nil 46 | } 47 | 48 | func (d *Discovery) watcher(prefix string) { 49 | rch := d.client.Watch(context.TODO(), prefix, clientV3.WithPrefix()) 50 | fmt.Printf("监听前缀: %s ..\n", prefix) 51 | for wresp := range rch { 52 | for _, ev := range wresp.Events { 53 | switch ev.Type { 54 | case mvccpb.PUT: //修改或者新增 55 | fmt.Printf("修改或新增, key:%s, value:%s\n", string(ev.Kv.Key), string(ev.Kv.Value)) 56 | d.serverMap.Store(string(ev.Kv.Key), string(ev.Kv.Value)) 57 | case mvccpb.DELETE: //删除 58 | fmt.Printf("删除, key:%s, value:%s\n", string(ev.Kv.Key), string(ev.Kv.Value)) 59 | d.serverMap.Delete(string(ev.Kv.Key)) 60 | } 61 | } 62 | } 63 | } 64 | 65 | func (d *Discovery) Close() error { 66 | return d.client.Close() 67 | } 68 | 69 | // GetServices 获取服务列表 70 | func (d *Discovery) GetServices() []string { 71 | addrs := make([]string, 0) 72 | d.serverMap.Range(func(key, value interface{}) bool { 73 | addrs = append(addrs, value.(string)) 74 | return true 75 | }) 76 | return addrs 77 | } 78 | -------------------------------------------------------------------------------- /lib/etcd/register.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "GoChat/config" 5 | "context" 6 | "fmt" 7 | clientV3 "go.etcd.io/etcd/client/v3" 8 | "time" 9 | ) 10 | 11 | // Register 服务注册 12 | type Register struct { 13 | client *clientV3.Client // etcd client 14 | leaseID clientV3.LeaseID //租约ID 15 | keepAliveChan <-chan *clientV3.LeaseKeepAliveResponse // 租约 KeepAlive 相应chan 16 | key string // key 17 | val string // value 18 | } 19 | 20 | // RegisterServer 新建注册服务 21 | func RegisterServer(key string, value string, lease int64) error { 22 | client, err := clientV3.New(clientV3.Config{ 23 | Endpoints: config.GlobalConfig.ETCD.Endpoints, 24 | DialTimeout: time.Duration(config.GlobalConfig.ETCD.Timeout) * time.Second, 25 | }) 26 | if err != nil { 27 | fmt.Println("etcd err:", err) 28 | return err 29 | } 30 | 31 | ser := &Register{ 32 | client: client, 33 | key: key, 34 | val: value, 35 | } 36 | 37 | //申请租约设置时间keepalive 38 | if err = ser.putKeyWithLease(lease); err != nil { 39 | return err 40 | } 41 | 42 | //监听续租相应chan 43 | go ser.ListenLeaseRespChan() 44 | 45 | return nil 46 | } 47 | 48 | // putKeyWithLease 设置 key 和租约 49 | func (r *Register) putKeyWithLease(timeNum int64) error { 50 | //设置租约时间 51 | resp, err := r.client.Grant(context.TODO(), timeNum) 52 | if err != nil { 53 | return err 54 | } 55 | //注册服务并绑定租约 56 | _, err = r.client.Put(context.TODO(), r.key, r.val, clientV3.WithLease(resp.ID)) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | //设置续租 定期发送需求请求 62 | leaseRespChan, err := r.client.KeepAlive(context.TODO(), resp.ID) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | r.leaseID = resp.ID 68 | r.keepAliveChan = leaseRespChan 69 | return nil 70 | } 71 | 72 | // ListenLeaseRespChan 监听 续租情况 73 | func (r *Register) ListenLeaseRespChan() { 74 | defer r.close() 75 | 76 | //for leaseKeepResp := range r.keepAliveChan { 77 | // fmt.Printf("续租成功,leaseID:%d, Put key:%s,val:%s reps:+%v\n", r.leaseID, r.key, r.val, leaseKeepResp) 78 | //} 79 | for range r.keepAliveChan { 80 | } 81 | fmt.Printf("续约失败,leaseID:%d, Put key:%s,val:%s\n", r.leaseID, r.key, r.val) 82 | } 83 | 84 | // Close 撤销租约 85 | func (r *Register) close() error { 86 | //撤销租约 87 | if _, err := r.client.Revoke(context.Background(), r.leaseID); err != nil { 88 | return err 89 | } 90 | fmt.Printf("撤销租约成功, leaseID:%d, Put key:%s,val:%s\n", r.leaseID, r.key, r.val) 91 | return r.client.Close() 92 | } 93 | -------------------------------------------------------------------------------- /lib/mq/message.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "GoChat/model" 5 | "GoChat/pkg/mq" 6 | "fmt" 7 | "github.com/wagslane/go-rabbitmq" 8 | ) 9 | 10 | const ( 11 | MessageQueue = "message.queue" 12 | MessageRoutingKey = "message.routing.key" 13 | MessageExchangeName = "message.exchange.name" 14 | ) 15 | 16 | var ( 17 | MessageMQ *mq.Conn 18 | ) 19 | 20 | func InitMessageMQ(url string) { 21 | MessageMQ = mq.InitRabbitMQ(url, MessageCreateHandler, MessageQueue, MessageRoutingKey, MessageExchangeName) 22 | } 23 | 24 | func MessageCreateHandler(d rabbitmq.Delivery) rabbitmq.Action { 25 | messageModels := model.ProtoMarshalToMessage(d.Body) 26 | if messageModels == nil { 27 | fmt.Println("空的") 28 | return rabbitmq.NackDiscard 29 | } 30 | err := model.CreateMessage(messageModels...) 31 | if err != nil { 32 | fmt.Println("[MessageCreateHandler] model.CreateMessage 失败,err:", err) 33 | return rabbitmq.NackDiscard 34 | } 35 | 36 | //fmt.Println("处理完消息:", string(d.Body)) 37 | return rabbitmq.Ack 38 | } 39 | -------------------------------------------------------------------------------- /lib/mq/message_test.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "GoChat/model" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestMessageMQ(t *testing.T) { 11 | url := "amqp://guest:guest@localhost:5672" 12 | InitMessageMQ(url) 13 | 14 | msgs := make([]*model.Message, 0) 15 | msgs = append(msgs, &model.Message{ 16 | ID: 1, 17 | UserID: 2, 18 | SenderID: 3, 19 | SessionType: 4, 20 | ReceiverId: 5, 21 | MessageType: 6, 22 | Content: []byte("我去"), 23 | Seq: 7, 24 | SendTime: time.Now(), 25 | CreateTime: time.Now(), 26 | UpdateTime: time.Now(), 27 | }) 28 | data, err := json.Marshal(msgs) 29 | if err != nil { 30 | panic(err) 31 | } 32 | timer := time.NewTicker(time.Second) 33 | for { 34 | select { 35 | case <-timer.C: 36 | err = MessageMQ.Publish(data) 37 | if err != nil { 38 | panic(err) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/lib/mq" 6 | "GoChat/pkg/db" 7 | "GoChat/pkg/etcd" 8 | "GoChat/router" 9 | "GoChat/service/rpc_server" 10 | ) 11 | 12 | func main() { 13 | // 初始化 14 | config.InitConfig("./app.yaml") 15 | db.InitMySQL(config.GlobalConfig.MySQL.DNS) 16 | db.InitRedis(config.GlobalConfig.Redis.Addr, config.GlobalConfig.Redis.Password) 17 | mq.InitMessageMQ(config.GlobalConfig.RabbitMQ.URL) 18 | 19 | // 初始化服务注册发现 20 | go etcd.InitETCD() 21 | 22 | // 启动 http 服务 23 | go router.HTTPRouter() 24 | 25 | // 启动 rpc 服务 26 | go rpc_server.InitRPCServer() 27 | 28 | // 启动 websocket 服务 29 | router.WSRouter() 30 | } 31 | -------------------------------------------------------------------------------- /model/friend.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "time" 6 | ) 7 | 8 | type Friend struct { 9 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 10 | UserID uint64 `gorm:"not null;comment:'用户id'" json:"user_id"` 11 | FriendID uint64 `gorm:"not null;comment:'好友id'" json:"friend_id"` 12 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 13 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 14 | } 15 | 16 | func (*Friend) TableName() string { 17 | return "friend" 18 | } 19 | 20 | // IsFriend 查询是否为好友关系 21 | func IsFriend(userId, friendId uint64) (bool, error) { 22 | var cnt int64 23 | err := db.DB.Model(&Friend{}).Where("user_id = ? and friend_id = ?", userId, friendId). 24 | Or("friend_id = ? and user_id = ?", userId, friendId). // 反查 25 | Count(&cnt).Error 26 | return cnt > 0, err 27 | } 28 | 29 | func CreateFriend(friend *Friend) error { 30 | return db.DB.Create(friend).Error 31 | } 32 | -------------------------------------------------------------------------------- /model/group.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "gorm.io/gorm" 6 | "time" 7 | ) 8 | 9 | type Group struct { 10 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 11 | Name string `gorm:"not null;comment:'群组名称'" json:"name"` 12 | OwnerID uint64 `gorm:"not null;comment:'群主id'" json:"owner_id"` 13 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 14 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 15 | } 16 | 17 | func (*Group) TableName() string { 18 | return "group" 19 | } 20 | 21 | func CreateGroup(group *Group, ids []uint64) error { 22 | return db.DB.Transaction(func(tx *gorm.DB) error { 23 | err := tx.Create(group).Error 24 | if err != nil { 25 | return err 26 | } 27 | 28 | groupUsers := make([]*GroupUser, 0, len(ids)) 29 | for _, id := range ids { 30 | groupUsers = append(groupUsers, &GroupUser{ 31 | GroupID: group.ID, 32 | UserID: id, 33 | }) 34 | } 35 | return tx.Create(groupUsers).Error 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /model/group_user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "time" 6 | ) 7 | 8 | type GroupUser struct { 9 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 10 | GroupID uint64 `gorm:"not null;comment:'组id'" json:"group_id"` 11 | UserID uint64 `gorm:"not null;comment:'用户id'" json:"user_id"` 12 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 13 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 14 | } 15 | 16 | func (*GroupUser) TableName() string { 17 | return "group_user" 18 | } 19 | 20 | // IsBelongToGroup 验证用户是否属于群 21 | func IsBelongToGroup(userId, groupId uint64) (bool, error) { 22 | var cnt int64 23 | err := db.DB.Model(&GroupUser{}). 24 | Where("user_id = ? and group_id = ?", userId, groupId). 25 | Count(&cnt).Error 26 | return cnt > 0, err 27 | } 28 | 29 | func GetGroupUserIdsByGroupId(groupId uint64) ([]uint64, error) { 30 | var ids []uint64 31 | err := db.DB.Model(&GroupUser{}).Where("group_id = ?", groupId).Pluck("user_id", &ids).Error 32 | return ids, err 33 | } 34 | -------------------------------------------------------------------------------- /model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "GoChat/pkg/protocol/pb" 6 | "fmt" 7 | "google.golang.org/protobuf/proto" 8 | "google.golang.org/protobuf/types/known/timestamppb" 9 | "time" 10 | ) 11 | 12 | const MessageLimit = 50 // 最大消息同步数量 13 | 14 | // Message 单聊消息 15 | type Message struct { 16 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 17 | UserID uint64 `gorm:"not null;comment:'用户id,指接受者用户id'" json:"user_id"` 18 | SenderID uint64 `gorm:"not null;comment:'发送者用户id'"` 19 | SessionType int8 `gorm:"not null;comment:'聊天类型,群聊/单聊'" json:"session_type"` 20 | ReceiverId uint64 `gorm:"not null;comment:'接收者id,群聊id/用户id'" json:"receiver_id"` 21 | MessageType int8 `gorm:"not null;comment:'消息类型,语言、文字、图片'" json:"message_type"` 22 | Content []byte `gorm:"not null;comment:'消息内容'" json:"content"` 23 | Seq uint64 `gorm:"not null;comment:'消息序列号'" json:"seq"` 24 | SendTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'消息发送时间'" json:"send_time"` 25 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 26 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 27 | } 28 | 29 | func (*Message) TableName() string { 30 | return "message" 31 | } 32 | 33 | func ProtoMarshalToMessage(data []byte) []*Message { 34 | var messages []*Message 35 | mqMessages := &pb.MQMessages{} 36 | err := proto.Unmarshal(data, mqMessages) 37 | if err != nil { 38 | fmt.Println("json.Unmarshal(mqMessages) 失败,err:", err) 39 | return nil 40 | } 41 | for _, mqMessage := range mqMessages.Messages { 42 | message := &Message{ 43 | UserID: mqMessage.UserId, 44 | SenderID: mqMessage.SenderId, 45 | SessionType: int8(mqMessage.SessionType), 46 | ReceiverId: mqMessage.ReceiverId, 47 | MessageType: int8(mqMessage.MessageType), 48 | Content: mqMessage.Content, 49 | Seq: mqMessage.Seq, 50 | SendTime: mqMessage.SendTime.AsTime(), 51 | } 52 | messages = append(messages, message) 53 | } 54 | return messages 55 | } 56 | 57 | func MessageToProtoMarshal(messages ...*Message) []byte { 58 | if len(messages) == 0 { 59 | return nil 60 | } 61 | var mqMessage []*pb.MQMessage 62 | for _, message := range messages { 63 | mqMessage = append(mqMessage, &pb.MQMessage{ 64 | UserId: message.UserID, 65 | SenderId: message.SenderID, 66 | SessionType: int32(message.SessionType), 67 | ReceiverId: message.ReceiverId, 68 | MessageType: int32(message.MessageType), 69 | Content: message.Content, 70 | Seq: message.Seq, 71 | SendTime: timestamppb.New(message.SendTime), 72 | }) 73 | } 74 | bytes, err := proto.Marshal(&pb.MQMessages{Messages: mqMessage}) 75 | if err != nil { 76 | fmt.Println("json.Marshal(messages) 失败,err:", err) 77 | return nil 78 | } 79 | return bytes 80 | } 81 | 82 | func MessagesToPB(messages []Message) []*pb.Message { 83 | pbMessages := make([]*pb.Message, 0, len(messages)) 84 | for _, message := range messages { 85 | pbMessages = append(pbMessages, &pb.Message{ 86 | SessionType: pb.SessionType(message.SessionType), 87 | ReceiverId: message.ReceiverId, 88 | SenderId: message.SenderID, 89 | MessageType: pb.MessageType(message.MessageType), 90 | Content: message.Content, 91 | Seq: message.Seq, 92 | }) 93 | } 94 | return pbMessages 95 | } 96 | 97 | func CreateMessage(msgs ...*Message) error { 98 | return db.DB.Create(msgs).Error 99 | } 100 | 101 | func ListByUserIdAndSeq(userId, seq uint64, limit int) ([]Message, bool, error) { 102 | var cnt int64 103 | err := db.DB.Model(&Message{}).Where("user_id = ? and seq > ?", userId, seq). 104 | Count(&cnt).Error 105 | if err != nil { 106 | return nil, false, err 107 | } 108 | if cnt == 0 { 109 | return nil, false, nil 110 | } 111 | 112 | var messages []Message 113 | err = db.DB.Model(&Message{}).Where("user_id = ? and seq > ?", userId, seq). 114 | Limit(limit).Order("seq ASC").Find(&messages).Error 115 | if err != nil { 116 | return nil, false, err 117 | } 118 | return messages, cnt > int64(limit), nil 119 | } 120 | -------------------------------------------------------------------------------- /model/uid.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type UID struct { 6 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 7 | BusinessID string `gorm:"not null;uniqueIndex:uk_business_id;comment:'业务id'" json:"business_id"` 8 | MaxID uint64 `gorm:"default:NULL;comment:'最大id'" json:"max_id"` 9 | Step int `gorm:"default:NULL;comment:'步长'" json:"step"` 10 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 11 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 12 | } 13 | 14 | func (u *UID) TableName() string { 15 | return "uid" 16 | } 17 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | ID uint64 `gorm:"primary_key;auto_increment;comment:'自增主键'" json:"id"` 10 | PhoneNumber string `gorm:"not null;unique;comment:'手机号'" json:"phone_number"` 11 | Nickname string `gorm:"not null;comment:'昵称'" json:"nickname"` 12 | Password string `gorm:"not null;comment:'密码'" json:"-"` 13 | CreateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:'创建时间'" json:"create_time"` 14 | UpdateTime time.Time `gorm:"not null;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'" json:"update_time"` 15 | } 16 | 17 | func (*User) TableName() string { 18 | return "user" 19 | } 20 | 21 | func GetUserCountByPhone(phoneNumber string) (int64, error) { 22 | var cnt int64 23 | err := db.DB.Model(&User{}).Where("phone_number = ?", phoneNumber).Count(&cnt).Error 24 | return cnt, err 25 | } 26 | 27 | func CreateUser(user *User) error { 28 | return db.DB.Create(user).Error 29 | } 30 | 31 | func GetUserByPhoneAndPassword(phoneNumber, password string) (*User, error) { 32 | user := new(User) 33 | err := db.DB.Model(&User{}).Where("phone_number = ? and password = ?", phoneNumber, password).First(user).Error 34 | return user, err 35 | } 36 | 37 | func GetUserById(id uint64) (*User, error) { 38 | user := new(User) 39 | err := db.DB.Model(&User{}).Where("id = ?", id).First(user).Error 40 | return user, err 41 | } 42 | 43 | func GetUserIdByIds(ids []uint64) ([]uint64, error) { 44 | var newIds []uint64 45 | m := make(map[uint64]struct{}, len(ids)) 46 | for i := 0; i < len(ids); i += 1000 { 47 | var tmp []uint64 48 | end := i + 1000 49 | if end > len(ids) { 50 | end = len(ids) 51 | } 52 | subIds := ids[i:end] 53 | err := db.DB.Model(&User{}).Where("id in (?)", subIds).Pluck("id", &tmp).Error 54 | if err != nil { 55 | return nil, err 56 | } 57 | for _, id := range tmp { 58 | m[id] = struct{}{} 59 | } 60 | } 61 | for id := range m { 62 | newIds = append(newIds, id) 63 | } 64 | return newIds, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "GoChat/pkg/logger" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | 8 | "moul.io/zapgorm2" 9 | "time" 10 | ) 11 | 12 | var ( 13 | DB *gorm.DB 14 | ) 15 | 16 | func InitMySQL(dataSource string) { 17 | logger.Logger.Info("mysql init...") 18 | var err error 19 | newLogger := zapgorm2.New(logger.Logger) 20 | newLogger.SetAsDefault() 21 | 22 | DB, err = gorm.Open(mysql.Open(dataSource), 23 | &gorm.Config{ 24 | Logger: newLogger, 25 | }) 26 | if err != nil { 27 | panic(err) 28 | } 29 | sqlDB, err := DB.DB() 30 | if err != nil { 31 | panic(err) 32 | } 33 | // SetMaxIdleConns 用于设置连接池中空闲连接的最大数量 34 | sqlDB.SetMaxIdleConns(20) 35 | 36 | // SetMaxOpenConns 设置打开数据库连接的最大数量 37 | sqlDB.SetMaxOpenConns(30) 38 | 39 | // SetConnMaxLifetime 设置了连接可复用的最大时间 40 | sqlDB.SetConnMaxLifetime(time.Hour) 41 | logger.Logger.Info("mysql init ok") 42 | } 43 | -------------------------------------------------------------------------------- /pkg/db/redis.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "GoChat/pkg/logger" 5 | "context" 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | var ( 10 | RDB *redis.Client 11 | ) 12 | 13 | func InitRedis(addr, password string) { 14 | logger.Logger.Debug("Redis init ...") 15 | RDB = redis.NewClient(&redis.Options{ 16 | Addr: addr, 17 | DB: 0, 18 | Password: password, 19 | PoolSize: 30, 20 | MinIdleConns: 30, 21 | }) 22 | err := RDB.Ping(context.Background()).Err() 23 | if err != nil { 24 | panic(err) 25 | } 26 | logger.Logger.Debug("Redis init ok") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/db/redis_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "GoChat/config" 5 | "testing" 6 | ) 7 | 8 | func TestRedis(t *testing.T) { 9 | config.InitConfig("../../app.yaml") 10 | InitRedis(config.GlobalConfig.Redis.Addr, config.GlobalConfig.Redis.Password) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pkg/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "GoChat/common" 5 | "GoChat/config" 6 | "GoChat/lib/etcd" 7 | "GoChat/pkg/logger" 8 | "fmt" 9 | "go.uber.org/zap" 10 | "time" 11 | ) 12 | 13 | var ( 14 | DiscoverySer *etcd.Discovery 15 | ) 16 | 17 | // InitETCD 初始化服务注册发现 18 | // 1. 初始化服务注册,将自己当前启动的 RPC 端口注册到 etcd 19 | // 2. 初始化服务发现,启动 watcher 监听所有 RPC 端口,以便有需要时能直接获取当前注册在 ETCD 的服务 20 | func InitETCD() { 21 | hostPort := fmt.Sprintf("%s:%s", config.GlobalConfig.App.IP, config.GlobalConfig.App.RPCPort) 22 | logger.Logger.Info("注册服务", zap.String("hostport", hostPort)) 23 | 24 | // 注册服务并设置 k-v 租约 25 | err := etcd.RegisterServer(common.EtcdServerList+hostPort, hostPort, 5) 26 | if err != nil { 27 | return 28 | } 29 | 30 | time.Sleep(100 * time.Millisecond) 31 | 32 | DiscoverySer, err = etcd.NewDiscovery() 33 | if err != nil { 34 | return 35 | } 36 | 37 | // 阻塞监听 38 | DiscoverySer.WatchService(common.EtcdServerList) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/etcd/etcd_test.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "GoChat/common" 5 | "GoChat/config" 6 | "GoChat/lib/etcd" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDiscovery(t *testing.T) { 12 | config.InitConfig("../../app.yaml") 13 | 14 | // 创建一个新的 Discovery 实例 15 | srv, err := etcd.NewDiscovery() 16 | if err != nil { 17 | t.Fatalf("failed to create discovery: %v", err) 18 | } 19 | defer srv.Close() 20 | 21 | // 注册两个 k-v 22 | err = etcd.RegisterServer(common.EtcdServerList+"888", "888", 5) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | err = etcd.RegisterServer(common.EtcdServerList+"666", "666", 5) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // 为一个存在的前缀启动 WatchService,并验证 GetServices 返回的服务列表是否正确 33 | go srv.WatchService(common.EtcdServerList) 34 | 35 | // 等待 watch 36 | time.Sleep(time.Second) 37 | 38 | services := srv.GetServices() 39 | if len(services) != 2 { 40 | t.Error("注册服务不足 2 个") 41 | for _, service := range services { 42 | t.Log(service) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | "os" 8 | "time" 9 | ) 10 | 11 | const ( 12 | Console = "console" 13 | File = "file" 14 | ) 15 | 16 | var ( 17 | // Logger 性能更好但是对使用者不方便,每次需要使用 zap.xxx 传入类型 18 | Logger *zap.Logger 19 | // Sugar 性能稍差但是可以不用指定传入类型 20 | Sugar *zap.SugaredLogger 21 | ) 22 | 23 | // 编码器(如何写入日志) 24 | func logEncoder() zapcore.Encoder { 25 | timeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 26 | enc.AppendString(t.Format("2006-01-02 15:04:05.000")) 27 | } 28 | 29 | encoderConfig := zapcore.EncoderConfig{ 30 | TimeKey: "T", 31 | LevelKey: "L", 32 | NameKey: "N", 33 | CallerKey: "C", 34 | MessageKey: "M", 35 | StacktraceKey: "S", 36 | LineEnding: zapcore.DefaultLineEnding, 37 | EncodeLevel: zapcore.CapitalLevelEncoder, 38 | EncodeTime: timeEncoder, 39 | EncodeDuration: zapcore.StringDurationEncoder, 40 | EncodeCaller: zapcore.ShortCallerEncoder, 41 | } 42 | 43 | return zapcore.NewConsoleEncoder(encoderConfig) 44 | } 45 | 46 | // 指定将日志写到哪里去 47 | func logWriterSyncer() zapcore.WriteSyncer { 48 | // 切割归档日志文件 49 | return zapcore.AddSync(&lumberjack.Logger{ 50 | Filename: "./log/im.log", 51 | MaxSize: 1024, // 日志文件的最大大小(MB) 52 | MaxAge: 7, // 保留旧文件的最大天数 53 | MaxBackups: 10, // 保留旧文档的最大个数 54 | LocalTime: false, 55 | Compress: false, // 是否压缩旧文件 56 | }) 57 | } 58 | 59 | func InitLogger(target string, level zapcore.Level) { 60 | w := logWriterSyncer() 61 | var writeSyncer zapcore.WriteSyncer 62 | // 打印在控制台 63 | if target == Console { 64 | writeSyncer = zapcore.AddSync(os.Stdout) 65 | } else if target == File { 66 | writeSyncer = zapcore.NewMultiWriteSyncer(w) 67 | } 68 | 69 | core := zapcore.NewCore( 70 | logEncoder(), // 怎么写 71 | writeSyncer, // 写到哪 72 | level, // 日志级别 73 | ) 74 | 75 | Logger = zap.New(core, zap.AddCaller()) // 打印调用方信息 76 | Sugar = Logger.Sugar() 77 | } 78 | -------------------------------------------------------------------------------- /pkg/middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "GoChat/pkg/util" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func AuthCheck() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | token := c.GetHeader("token") 12 | userClaims, err := util.AnalyseToken(token) 13 | if err != nil { 14 | c.Abort() 15 | c.JSON(http.StatusOK, gin.H{ 16 | "code": -1, 17 | "msg": "用户认证未通过", 18 | }) 19 | return 20 | } 21 | c.Set("user_claims", userClaims) 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/mq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wagslane/go-rabbitmq" 6 | ) 7 | 8 | type Conn struct { 9 | conn *rabbitmq.Conn 10 | consumer *rabbitmq.Consumer 11 | publisher *rabbitmq.Publisher 12 | queue string 13 | routingKey string 14 | exchangeName string 15 | } 16 | 17 | // InitRabbitMQ 初始化连接 18 | // 启动消费者、初始化生产者 19 | func InitRabbitMQ(url string, f rabbitmq.Handler, queue, routingKey, exchangeName string) *Conn { 20 | // 初始化连接 21 | conn, err := rabbitmq.NewConn(url) 22 | if err != nil { 23 | panic(err) 24 | } 25 | // 消费者,注册时已经启动了 26 | consumer, err := rabbitmq.NewConsumer( 27 | conn, 28 | f, // 实际进行消费处理的函数 29 | queue, // 队列名称 30 | rabbitmq.WithConsumerOptionsRoutingKey(routingKey), // routing-key 31 | rabbitmq.WithConsumerOptionsExchangeName(exchangeName), // exchange 名称 32 | rabbitmq.WithConsumerOptionsExchangeDeclare, // 声明交换器 33 | ) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // 生产者 39 | publisher, err := rabbitmq.NewPublisher( 40 | conn, 41 | rabbitmq.WithPublisherOptionsExchangeName(exchangeName), // exchange 名称 42 | rabbitmq.WithPublisherOptionsExchangeDeclare, // 声明交换器 43 | ) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // 连接被拒绝 49 | publisher.NotifyReturn(func(r rabbitmq.Return) { 50 | //log.Printf("message returned from server: %s", string(r.Body)) 51 | }) 52 | 53 | // 提交确认 54 | publisher.NotifyPublish(func(c rabbitmq.Confirmation) { 55 | //log.Printf("message confirmed from server. tag: %v, ack: %v", c.DeliveryTag, c.Ack) 56 | }) 57 | 58 | return &Conn{ 59 | conn: conn, 60 | consumer: consumer, 61 | publisher: publisher, 62 | queue: queue, 63 | routingKey: routingKey, 64 | exchangeName: exchangeName, 65 | } 66 | } 67 | 68 | // Publish 发送消息,该消息实际由执行 InitRabbitMQ 注册时传入的 f 消费 69 | func (c *Conn) Publish(data []byte) error { 70 | if data == nil || len(data) == 0 { 71 | fmt.Println("data 为空,publish 不发送") 72 | return nil 73 | } 74 | return c.publisher.Publish( 75 | data, 76 | []string{c.routingKey}, 77 | rabbitmq.WithPublishOptionsContentType("application/json"), 78 | rabbitmq.WithPublishOptionsPersistentDelivery, // 消息持久化 79 | rabbitmq.WithPublishOptionsExchange(c.exchangeName), // 要发送的 exchange 80 | ) 81 | } 82 | 83 | func (c *Conn) Close() { 84 | c.conn.Close() 85 | c.consumer.Close() 86 | c.publisher.Close() 87 | } 88 | -------------------------------------------------------------------------------- /pkg/protocol/pb/conn.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.20.1 5 | // source: conn.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | emptypb "google.golang.org/protobuf/types/known/emptypb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type DeliverMessageReq struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | ReceiverId uint64 `protobuf:"varint,1,opt,name=receiver_id,json=receiverId,proto3" json:"receiver_id,omitempty"` // 消息接收者 30 | Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // 要投递的消息 31 | } 32 | 33 | func (x *DeliverMessageReq) Reset() { 34 | *x = DeliverMessageReq{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_conn_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *DeliverMessageReq) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*DeliverMessageReq) ProtoMessage() {} 47 | 48 | func (x *DeliverMessageReq) ProtoReflect() protoreflect.Message { 49 | mi := &file_conn_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 DeliverMessageReq.ProtoReflect.Descriptor instead. 61 | func (*DeliverMessageReq) Descriptor() ([]byte, []int) { 62 | return file_conn_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *DeliverMessageReq) GetReceiverId() uint64 { 66 | if x != nil { 67 | return x.ReceiverId 68 | } 69 | return 0 70 | } 71 | 72 | func (x *DeliverMessageReq) GetData() []byte { 73 | if x != nil { 74 | return x.Data 75 | } 76 | return nil 77 | } 78 | 79 | type DeliverMessageAllReq struct { 80 | state protoimpl.MessageState 81 | sizeCache protoimpl.SizeCache 82 | unknownFields protoimpl.UnknownFields 83 | 84 | ReceiverId_2Data map[uint64][]byte `protobuf:"bytes,1,rep,name=receiver_id_2_data,json=receiverId2Data,proto3" json:"receiver_id_2_data,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // 消息接受者到要投递的消息的映射 85 | } 86 | 87 | func (x *DeliverMessageAllReq) Reset() { 88 | *x = DeliverMessageAllReq{} 89 | if protoimpl.UnsafeEnabled { 90 | mi := &file_conn_proto_msgTypes[1] 91 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 92 | ms.StoreMessageInfo(mi) 93 | } 94 | } 95 | 96 | func (x *DeliverMessageAllReq) String() string { 97 | return protoimpl.X.MessageStringOf(x) 98 | } 99 | 100 | func (*DeliverMessageAllReq) ProtoMessage() {} 101 | 102 | func (x *DeliverMessageAllReq) ProtoReflect() protoreflect.Message { 103 | mi := &file_conn_proto_msgTypes[1] 104 | if protoimpl.UnsafeEnabled && x != nil { 105 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 106 | if ms.LoadMessageInfo() == nil { 107 | ms.StoreMessageInfo(mi) 108 | } 109 | return ms 110 | } 111 | return mi.MessageOf(x) 112 | } 113 | 114 | // Deprecated: Use DeliverMessageAllReq.ProtoReflect.Descriptor instead. 115 | func (*DeliverMessageAllReq) Descriptor() ([]byte, []int) { 116 | return file_conn_proto_rawDescGZIP(), []int{1} 117 | } 118 | 119 | func (x *DeliverMessageAllReq) GetReceiverId_2Data() map[uint64][]byte { 120 | if x != nil { 121 | return x.ReceiverId_2Data 122 | } 123 | return nil 124 | } 125 | 126 | var File_conn_proto protoreflect.FileDescriptor 127 | 128 | var file_conn_proto_rawDesc = []byte{ 129 | 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 130 | 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 131 | 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x48, 0x0a, 132 | 0x11, 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 133 | 0x65, 0x71, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x69, 134 | 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 135 | 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 136 | 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb6, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x69, 137 | 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x71, 138 | 0x12, 0x5a, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 139 | 0x32, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 140 | 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 141 | 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x2e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x49, 142 | 0x64, 0x32, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x72, 0x65, 0x63, 143 | 0x65, 0x69, 0x76, 0x65, 0x72, 0x49, 0x64, 0x32, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x42, 0x0a, 0x14, 144 | 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x49, 0x64, 0x32, 0x44, 0x61, 0x74, 0x61, 0x45, 145 | 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 146 | 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 147 | 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 148 | 0x32, 0x91, 0x01, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x3f, 0x0a, 0x0e, 149 | 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 150 | 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 151 | 0x67, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 152 | 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, 153 | 0x11, 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x41, 154 | 0x6c, 0x6c, 0x12, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x4d, 155 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x67, 156 | 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 157 | 0x6d, 0x70, 0x74, 0x79, 0x42, 0x18, 0x5a, 0x16, 0x47, 0x6f, 0x43, 0x68, 0x61, 0x74, 0x2f, 0x70, 158 | 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x70, 0x62, 0x62, 0x06, 159 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 160 | } 161 | 162 | var ( 163 | file_conn_proto_rawDescOnce sync.Once 164 | file_conn_proto_rawDescData = file_conn_proto_rawDesc 165 | ) 166 | 167 | func file_conn_proto_rawDescGZIP() []byte { 168 | file_conn_proto_rawDescOnce.Do(func() { 169 | file_conn_proto_rawDescData = protoimpl.X.CompressGZIP(file_conn_proto_rawDescData) 170 | }) 171 | return file_conn_proto_rawDescData 172 | } 173 | 174 | var file_conn_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 175 | var file_conn_proto_goTypes = []interface{}{ 176 | (*DeliverMessageReq)(nil), // 0: pb.DeliverMessageReq 177 | (*DeliverMessageAllReq)(nil), // 1: pb.DeliverMessageAllReq 178 | nil, // 2: pb.DeliverMessageAllReq.ReceiverId2DataEntry 179 | (*emptypb.Empty)(nil), // 3: google.protobuf.Empty 180 | } 181 | var file_conn_proto_depIdxs = []int32{ 182 | 2, // 0: pb.DeliverMessageAllReq.receiver_id_2_data:type_name -> pb.DeliverMessageAllReq.ReceiverId2DataEntry 183 | 0, // 1: pb.Connect.DeliverMessage:input_type -> pb.DeliverMessageReq 184 | 1, // 2: pb.Connect.DeliverMessageAll:input_type -> pb.DeliverMessageAllReq 185 | 3, // 3: pb.Connect.DeliverMessage:output_type -> google.protobuf.Empty 186 | 3, // 4: pb.Connect.DeliverMessageAll:output_type -> google.protobuf.Empty 187 | 3, // [3:5] is the sub-list for method output_type 188 | 1, // [1:3] is the sub-list for method input_type 189 | 1, // [1:1] is the sub-list for extension type_name 190 | 1, // [1:1] is the sub-list for extension extendee 191 | 0, // [0:1] is the sub-list for field type_name 192 | } 193 | 194 | func init() { file_conn_proto_init() } 195 | func file_conn_proto_init() { 196 | if File_conn_proto != nil { 197 | return 198 | } 199 | if !protoimpl.UnsafeEnabled { 200 | file_conn_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 201 | switch v := v.(*DeliverMessageReq); i { 202 | case 0: 203 | return &v.state 204 | case 1: 205 | return &v.sizeCache 206 | case 2: 207 | return &v.unknownFields 208 | default: 209 | return nil 210 | } 211 | } 212 | file_conn_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 213 | switch v := v.(*DeliverMessageAllReq); i { 214 | case 0: 215 | return &v.state 216 | case 1: 217 | return &v.sizeCache 218 | case 2: 219 | return &v.unknownFields 220 | default: 221 | return nil 222 | } 223 | } 224 | } 225 | type x struct{} 226 | out := protoimpl.TypeBuilder{ 227 | File: protoimpl.DescBuilder{ 228 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 229 | RawDescriptor: file_conn_proto_rawDesc, 230 | NumEnums: 0, 231 | NumMessages: 3, 232 | NumExtensions: 0, 233 | NumServices: 1, 234 | }, 235 | GoTypes: file_conn_proto_goTypes, 236 | DependencyIndexes: file_conn_proto_depIdxs, 237 | MessageInfos: file_conn_proto_msgTypes, 238 | }.Build() 239 | File_conn_proto = out.File 240 | file_conn_proto_rawDesc = nil 241 | file_conn_proto_goTypes = nil 242 | file_conn_proto_depIdxs = nil 243 | } 244 | -------------------------------------------------------------------------------- /pkg/protocol/pb/conn_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.20.1 5 | // source: conn.proto 6 | 7 | package pb 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 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | // ConnectClient is the client API for Connect service. 23 | // 24 | // 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. 25 | type ConnectClient interface { 26 | // 私聊消息投递 27 | DeliverMessage(ctx context.Context, in *DeliverMessageReq, opts ...grpc.CallOption) (*emptypb.Empty, error) 28 | // 群聊消息投递 29 | DeliverMessageAll(ctx context.Context, in *DeliverMessageAllReq, opts ...grpc.CallOption) (*emptypb.Empty, error) 30 | } 31 | 32 | type connectClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewConnectClient(cc grpc.ClientConnInterface) ConnectClient { 37 | return &connectClient{cc} 38 | } 39 | 40 | func (c *connectClient) DeliverMessage(ctx context.Context, in *DeliverMessageReq, opts ...grpc.CallOption) (*emptypb.Empty, error) { 41 | out := new(emptypb.Empty) 42 | err := c.cc.Invoke(ctx, "/pb.Connect/DeliverMessage", in, out, opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return out, nil 47 | } 48 | 49 | func (c *connectClient) DeliverMessageAll(ctx context.Context, in *DeliverMessageAllReq, opts ...grpc.CallOption) (*emptypb.Empty, error) { 50 | out := new(emptypb.Empty) 51 | err := c.cc.Invoke(ctx, "/pb.Connect/DeliverMessageAll", in, out, opts...) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return out, nil 56 | } 57 | 58 | // ConnectServer is the server API for Connect service. 59 | // All implementations must embed UnimplementedConnectServer 60 | // for forward compatibility 61 | type ConnectServer interface { 62 | // 私聊消息投递 63 | DeliverMessage(context.Context, *DeliverMessageReq) (*emptypb.Empty, error) 64 | // 群聊消息投递 65 | DeliverMessageAll(context.Context, *DeliverMessageAllReq) (*emptypb.Empty, error) 66 | mustEmbedUnimplementedConnectServer() 67 | } 68 | 69 | // UnimplementedConnectServer must be embedded to have forward compatible implementations. 70 | type UnimplementedConnectServer struct { 71 | } 72 | 73 | func (UnimplementedConnectServer) DeliverMessage(context.Context, *DeliverMessageReq) (*emptypb.Empty, error) { 74 | return nil, status.Errorf(codes.Unimplemented, "method DeliverMessage not implemented") 75 | } 76 | func (UnimplementedConnectServer) DeliverMessageAll(context.Context, *DeliverMessageAllReq) (*emptypb.Empty, error) { 77 | return nil, status.Errorf(codes.Unimplemented, "method DeliverMessageAll not implemented") 78 | } 79 | func (UnimplementedConnectServer) mustEmbedUnimplementedConnectServer() {} 80 | 81 | // UnsafeConnectServer may be embedded to opt out of forward compatibility for this service. 82 | // Use of this interface is not recommended, as added methods to ConnectServer will 83 | // result in compilation errors. 84 | type UnsafeConnectServer interface { 85 | mustEmbedUnimplementedConnectServer() 86 | } 87 | 88 | func RegisterConnectServer(s grpc.ServiceRegistrar, srv ConnectServer) { 89 | s.RegisterService(&Connect_ServiceDesc, srv) 90 | } 91 | 92 | func _Connect_DeliverMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 93 | in := new(DeliverMessageReq) 94 | if err := dec(in); err != nil { 95 | return nil, err 96 | } 97 | if interceptor == nil { 98 | return srv.(ConnectServer).DeliverMessage(ctx, in) 99 | } 100 | info := &grpc.UnaryServerInfo{ 101 | Server: srv, 102 | FullMethod: "/pb.Connect/DeliverMessage", 103 | } 104 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 105 | return srv.(ConnectServer).DeliverMessage(ctx, req.(*DeliverMessageReq)) 106 | } 107 | return interceptor(ctx, in, info, handler) 108 | } 109 | 110 | func _Connect_DeliverMessageAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 111 | in := new(DeliverMessageAllReq) 112 | if err := dec(in); err != nil { 113 | return nil, err 114 | } 115 | if interceptor == nil { 116 | return srv.(ConnectServer).DeliverMessageAll(ctx, in) 117 | } 118 | info := &grpc.UnaryServerInfo{ 119 | Server: srv, 120 | FullMethod: "/pb.Connect/DeliverMessageAll", 121 | } 122 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 123 | return srv.(ConnectServer).DeliverMessageAll(ctx, req.(*DeliverMessageAllReq)) 124 | } 125 | return interceptor(ctx, in, info, handler) 126 | } 127 | 128 | // Connect_ServiceDesc is the grpc.ServiceDesc for Connect service. 129 | // It's only intended for direct use with grpc.RegisterService, 130 | // and not to be introspected or modified (even as a copy) 131 | var Connect_ServiceDesc = grpc.ServiceDesc{ 132 | ServiceName: "pb.Connect", 133 | HandlerType: (*ConnectServer)(nil), 134 | Methods: []grpc.MethodDesc{ 135 | { 136 | MethodName: "DeliverMessage", 137 | Handler: _Connect_DeliverMessage_Handler, 138 | }, 139 | { 140 | MethodName: "DeliverMessageAll", 141 | Handler: _Connect_DeliverMessageAll_Handler, 142 | }, 143 | }, 144 | Streams: []grpc.StreamDesc{}, 145 | Metadata: "conn.proto", 146 | } 147 | -------------------------------------------------------------------------------- /pkg/protocol/pb/message.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.20.1 5 | // source: message.proto 6 | 7 | package pb 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 SessionType int32 25 | 26 | const ( 27 | SessionType_ST_UnKnow SessionType = 0 // 未知 28 | SessionType_ST_Single SessionType = 1 // 单聊 29 | SessionType_ST_Group SessionType = 2 // 群聊 30 | ) 31 | 32 | // Enum value maps for SessionType. 33 | var ( 34 | SessionType_name = map[int32]string{ 35 | 0: "ST_UnKnow", 36 | 1: "ST_Single", 37 | 2: "ST_Group", 38 | } 39 | SessionType_value = map[string]int32{ 40 | "ST_UnKnow": 0, 41 | "ST_Single": 1, 42 | "ST_Group": 2, 43 | } 44 | ) 45 | 46 | func (x SessionType) Enum() *SessionType { 47 | p := new(SessionType) 48 | *p = x 49 | return p 50 | } 51 | 52 | func (x SessionType) String() string { 53 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 54 | } 55 | 56 | func (SessionType) Descriptor() protoreflect.EnumDescriptor { 57 | return file_message_proto_enumTypes[0].Descriptor() 58 | } 59 | 60 | func (SessionType) Type() protoreflect.EnumType { 61 | return &file_message_proto_enumTypes[0] 62 | } 63 | 64 | func (x SessionType) Number() protoreflect.EnumNumber { 65 | return protoreflect.EnumNumber(x) 66 | } 67 | 68 | // Deprecated: Use SessionType.Descriptor instead. 69 | func (SessionType) EnumDescriptor() ([]byte, []int) { 70 | return file_message_proto_rawDescGZIP(), []int{0} 71 | } 72 | 73 | // 用户所发送内容的消息类型 74 | type MessageType int32 75 | 76 | const ( 77 | MessageType_MT_UnKnow MessageType = 0 // 未知 78 | MessageType_MT_Text MessageType = 1 // 文本类型消息 79 | MessageType_MT_Picture MessageType = 2 // 图片类型消息 80 | MessageType_MT_Voice MessageType = 3 // 语音类型消息 81 | ) 82 | 83 | // Enum value maps for MessageType. 84 | var ( 85 | MessageType_name = map[int32]string{ 86 | 0: "MT_UnKnow", 87 | 1: "MT_Text", 88 | 2: "MT_Picture", 89 | 3: "MT_Voice", 90 | } 91 | MessageType_value = map[string]int32{ 92 | "MT_UnKnow": 0, 93 | "MT_Text": 1, 94 | "MT_Picture": 2, 95 | "MT_Voice": 3, 96 | } 97 | ) 98 | 99 | func (x MessageType) Enum() *MessageType { 100 | p := new(MessageType) 101 | *p = x 102 | return p 103 | } 104 | 105 | func (x MessageType) String() string { 106 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 107 | } 108 | 109 | func (MessageType) Descriptor() protoreflect.EnumDescriptor { 110 | return file_message_proto_enumTypes[1].Descriptor() 111 | } 112 | 113 | func (MessageType) Type() protoreflect.EnumType { 114 | return &file_message_proto_enumTypes[1] 115 | } 116 | 117 | func (x MessageType) Number() protoreflect.EnumNumber { 118 | return protoreflect.EnumNumber(x) 119 | } 120 | 121 | // Deprecated: Use MessageType.Descriptor instead. 122 | func (MessageType) EnumDescriptor() ([]byte, []int) { 123 | return file_message_proto_rawDescGZIP(), []int{1} 124 | } 125 | 126 | // ACK 消息类型,先根据 Input/Output 的 type 解析出是 ACK,再根据 ACKType 判断是 ACK 的是什么消息 127 | type ACKType int32 128 | 129 | const ( 130 | ACKType_AT_UnKnow ACKType = 0 // 未知 131 | ACKType_AT_Up ACKType = 1 // 服务端回复客户端发来的消息 132 | ACKType_AT_Push ACKType = 2 // 客户端回复服务端发来的消息 133 | ACKType_AT_Login ACKType = 3 // 登录 134 | ) 135 | 136 | // Enum value maps for ACKType. 137 | var ( 138 | ACKType_name = map[int32]string{ 139 | 0: "AT_UnKnow", 140 | 1: "AT_Up", 141 | 2: "AT_Push", 142 | 3: "AT_Login", 143 | } 144 | ACKType_value = map[string]int32{ 145 | "AT_UnKnow": 0, 146 | "AT_Up": 1, 147 | "AT_Push": 2, 148 | "AT_Login": 3, 149 | } 150 | ) 151 | 152 | func (x ACKType) Enum() *ACKType { 153 | p := new(ACKType) 154 | *p = x 155 | return p 156 | } 157 | 158 | func (x ACKType) String() string { 159 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 160 | } 161 | 162 | func (ACKType) Descriptor() protoreflect.EnumDescriptor { 163 | return file_message_proto_enumTypes[2].Descriptor() 164 | } 165 | 166 | func (ACKType) Type() protoreflect.EnumType { 167 | return &file_message_proto_enumTypes[2] 168 | } 169 | 170 | func (x ACKType) Number() protoreflect.EnumNumber { 171 | return protoreflect.EnumNumber(x) 172 | } 173 | 174 | // Deprecated: Use ACKType.Descriptor instead. 175 | func (ACKType) EnumDescriptor() ([]byte, []int) { 176 | return file_message_proto_rawDescGZIP(), []int{2} 177 | } 178 | 179 | // 所有 websocket 的消息类型 180 | type CmdType int32 181 | 182 | const ( 183 | CmdType_CT_UnKnow CmdType = 0 // 未知 184 | CmdType_CT_Login CmdType = 1 // 连接注册,客户端向服务端发送,建立连接 185 | CmdType_CT_Heartbeat CmdType = 2 // 心跳,客户端向服务端发送,连接保活 186 | CmdType_CT_Message CmdType = 3 // 消息投递,可能是服务端发给客户端,也可能是客户端发给服务端 187 | CmdType_CT_ACK CmdType = 4 // ACK 188 | CmdType_CT_Sync CmdType = 5 // 离线消息同步 189 | ) 190 | 191 | // Enum value maps for CmdType. 192 | var ( 193 | CmdType_name = map[int32]string{ 194 | 0: "CT_UnKnow", 195 | 1: "CT_Login", 196 | 2: "CT_Heartbeat", 197 | 3: "CT_Message", 198 | 4: "CT_ACK", 199 | 5: "CT_Sync", 200 | } 201 | CmdType_value = map[string]int32{ 202 | "CT_UnKnow": 0, 203 | "CT_Login": 1, 204 | "CT_Heartbeat": 2, 205 | "CT_Message": 3, 206 | "CT_ACK": 4, 207 | "CT_Sync": 5, 208 | } 209 | ) 210 | 211 | func (x CmdType) Enum() *CmdType { 212 | p := new(CmdType) 213 | *p = x 214 | return p 215 | } 216 | 217 | func (x CmdType) String() string { 218 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 219 | } 220 | 221 | func (CmdType) Descriptor() protoreflect.EnumDescriptor { 222 | return file_message_proto_enumTypes[3].Descriptor() 223 | } 224 | 225 | func (CmdType) Type() protoreflect.EnumType { 226 | return &file_message_proto_enumTypes[3] 227 | } 228 | 229 | func (x CmdType) Number() protoreflect.EnumNumber { 230 | return protoreflect.EnumNumber(x) 231 | } 232 | 233 | // Deprecated: Use CmdType.Descriptor instead. 234 | func (CmdType) EnumDescriptor() ([]byte, []int) { 235 | return file_message_proto_rawDescGZIP(), []int{3} 236 | } 237 | 238 | // 上行消息(客户端发送给服务端)顶层消息 239 | // 使用: 240 | // 客户端发送前:先组装出下层消息例如 HeartBeatMsg,序列化作为 Input 的 data 值,再填写 type 值,序列化 Input 发送给服务端 241 | // 服务端收到后:反序列化成 Input,根据 type 值调用不同类型 handler,在 handler 中将 data 解析成其他例如 LoginMsg 类型消息,再做处理 242 | type Input struct { 243 | state protoimpl.MessageState 244 | sizeCache protoimpl.SizeCache 245 | unknownFields protoimpl.UnknownFields 246 | 247 | Type CmdType `protobuf:"varint,1,opt,name=type,proto3,enum=pb.CmdType" json:"type,omitempty"` // 消息类型,根据不同消息类型,可以将 data 解析成下面其他类型 248 | Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // 数据 249 | } 250 | 251 | func (x *Input) Reset() { 252 | *x = Input{} 253 | if protoimpl.UnsafeEnabled { 254 | mi := &file_message_proto_msgTypes[0] 255 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 256 | ms.StoreMessageInfo(mi) 257 | } 258 | } 259 | 260 | func (x *Input) String() string { 261 | return protoimpl.X.MessageStringOf(x) 262 | } 263 | 264 | func (*Input) ProtoMessage() {} 265 | 266 | func (x *Input) ProtoReflect() protoreflect.Message { 267 | mi := &file_message_proto_msgTypes[0] 268 | if protoimpl.UnsafeEnabled && x != nil { 269 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 270 | if ms.LoadMessageInfo() == nil { 271 | ms.StoreMessageInfo(mi) 272 | } 273 | return ms 274 | } 275 | return mi.MessageOf(x) 276 | } 277 | 278 | // Deprecated: Use Input.ProtoReflect.Descriptor instead. 279 | func (*Input) Descriptor() ([]byte, []int) { 280 | return file_message_proto_rawDescGZIP(), []int{0} 281 | } 282 | 283 | func (x *Input) GetType() CmdType { 284 | if x != nil { 285 | return x.Type 286 | } 287 | return CmdType_CT_UnKnow 288 | } 289 | 290 | func (x *Input) GetData() []byte { 291 | if x != nil { 292 | return x.Data 293 | } 294 | return nil 295 | } 296 | 297 | // 下行消息(服务端发送给客户端)顶层消息 298 | // 使用: 299 | // 服务端发送前:组装出下层消息例如 Message,序列化作为 Output 的 data 值,再填写其他值,序列化 Output 发送给客户端 300 | // 客户端收到后:反序列化成 Output,根据 type 值调用不同类型 handler,在 handler 中将 data 解析成其他例如 Message 类型消息,再做处理 301 | type Output struct { 302 | state protoimpl.MessageState 303 | sizeCache protoimpl.SizeCache 304 | unknownFields protoimpl.UnknownFields 305 | 306 | Type CmdType `protobuf:"varint,1,opt,name=type,proto3,enum=pb.CmdType" json:"type,omitempty"` // 消息类型,根据不同的消息类型,可以将 data 解析成下面其他类型 307 | Code int32 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` // 错误码 308 | CodeMsg string `protobuf:"bytes,3,opt,name=CodeMsg,proto3" json:"CodeMsg,omitempty"` // 错误码信息 309 | Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` // 数据 310 | } 311 | 312 | func (x *Output) Reset() { 313 | *x = Output{} 314 | if protoimpl.UnsafeEnabled { 315 | mi := &file_message_proto_msgTypes[1] 316 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 317 | ms.StoreMessageInfo(mi) 318 | } 319 | } 320 | 321 | func (x *Output) String() string { 322 | return protoimpl.X.MessageStringOf(x) 323 | } 324 | 325 | func (*Output) ProtoMessage() {} 326 | 327 | func (x *Output) ProtoReflect() protoreflect.Message { 328 | mi := &file_message_proto_msgTypes[1] 329 | if protoimpl.UnsafeEnabled && x != nil { 330 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 331 | if ms.LoadMessageInfo() == nil { 332 | ms.StoreMessageInfo(mi) 333 | } 334 | return ms 335 | } 336 | return mi.MessageOf(x) 337 | } 338 | 339 | // Deprecated: Use Output.ProtoReflect.Descriptor instead. 340 | func (*Output) Descriptor() ([]byte, []int) { 341 | return file_message_proto_rawDescGZIP(), []int{1} 342 | } 343 | 344 | func (x *Output) GetType() CmdType { 345 | if x != nil { 346 | return x.Type 347 | } 348 | return CmdType_CT_UnKnow 349 | } 350 | 351 | func (x *Output) GetCode() int32 { 352 | if x != nil { 353 | return x.Code 354 | } 355 | return 0 356 | } 357 | 358 | func (x *Output) GetCodeMsg() string { 359 | if x != nil { 360 | return x.CodeMsg 361 | } 362 | return "" 363 | } 364 | 365 | func (x *Output) GetData() []byte { 366 | if x != nil { 367 | return x.Data 368 | } 369 | return nil 370 | } 371 | 372 | // 下行消息批处理 373 | type OutputBatch struct { 374 | state protoimpl.MessageState 375 | sizeCache protoimpl.SizeCache 376 | unknownFields protoimpl.UnknownFields 377 | 378 | Outputs [][]byte `protobuf:"bytes,1,rep,name=outputs,proto3" json:"outputs,omitempty"` 379 | } 380 | 381 | func (x *OutputBatch) Reset() { 382 | *x = OutputBatch{} 383 | if protoimpl.UnsafeEnabled { 384 | mi := &file_message_proto_msgTypes[2] 385 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 386 | ms.StoreMessageInfo(mi) 387 | } 388 | } 389 | 390 | func (x *OutputBatch) String() string { 391 | return protoimpl.X.MessageStringOf(x) 392 | } 393 | 394 | func (*OutputBatch) ProtoMessage() {} 395 | 396 | func (x *OutputBatch) ProtoReflect() protoreflect.Message { 397 | mi := &file_message_proto_msgTypes[2] 398 | if protoimpl.UnsafeEnabled && x != nil { 399 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 400 | if ms.LoadMessageInfo() == nil { 401 | ms.StoreMessageInfo(mi) 402 | } 403 | return ms 404 | } 405 | return mi.MessageOf(x) 406 | } 407 | 408 | // Deprecated: Use OutputBatch.ProtoReflect.Descriptor instead. 409 | func (*OutputBatch) Descriptor() ([]byte, []int) { 410 | return file_message_proto_rawDescGZIP(), []int{2} 411 | } 412 | 413 | func (x *OutputBatch) GetOutputs() [][]byte { 414 | if x != nil { 415 | return x.Outputs 416 | } 417 | return nil 418 | } 419 | 420 | // 登录 421 | type LoginMsg struct { 422 | state protoimpl.MessageState 423 | sizeCache protoimpl.SizeCache 424 | unknownFields protoimpl.UnknownFields 425 | 426 | Token []byte `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // token 427 | } 428 | 429 | func (x *LoginMsg) Reset() { 430 | *x = LoginMsg{} 431 | if protoimpl.UnsafeEnabled { 432 | mi := &file_message_proto_msgTypes[3] 433 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 434 | ms.StoreMessageInfo(mi) 435 | } 436 | } 437 | 438 | func (x *LoginMsg) String() string { 439 | return protoimpl.X.MessageStringOf(x) 440 | } 441 | 442 | func (*LoginMsg) ProtoMessage() {} 443 | 444 | func (x *LoginMsg) ProtoReflect() protoreflect.Message { 445 | mi := &file_message_proto_msgTypes[3] 446 | if protoimpl.UnsafeEnabled && x != nil { 447 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 448 | if ms.LoadMessageInfo() == nil { 449 | ms.StoreMessageInfo(mi) 450 | } 451 | return ms 452 | } 453 | return mi.MessageOf(x) 454 | } 455 | 456 | // Deprecated: Use LoginMsg.ProtoReflect.Descriptor instead. 457 | func (*LoginMsg) Descriptor() ([]byte, []int) { 458 | return file_message_proto_rawDescGZIP(), []int{3} 459 | } 460 | 461 | func (x *LoginMsg) GetToken() []byte { 462 | if x != nil { 463 | return x.Token 464 | } 465 | return nil 466 | } 467 | 468 | // 心跳 469 | type HeartbeatMsg struct { 470 | state protoimpl.MessageState 471 | sizeCache protoimpl.SizeCache 472 | unknownFields protoimpl.UnknownFields 473 | } 474 | 475 | func (x *HeartbeatMsg) Reset() { 476 | *x = HeartbeatMsg{} 477 | if protoimpl.UnsafeEnabled { 478 | mi := &file_message_proto_msgTypes[4] 479 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 480 | ms.StoreMessageInfo(mi) 481 | } 482 | } 483 | 484 | func (x *HeartbeatMsg) String() string { 485 | return protoimpl.X.MessageStringOf(x) 486 | } 487 | 488 | func (*HeartbeatMsg) ProtoMessage() {} 489 | 490 | func (x *HeartbeatMsg) ProtoReflect() protoreflect.Message { 491 | mi := &file_message_proto_msgTypes[4] 492 | if protoimpl.UnsafeEnabled && x != nil { 493 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 494 | if ms.LoadMessageInfo() == nil { 495 | ms.StoreMessageInfo(mi) 496 | } 497 | return ms 498 | } 499 | return mi.MessageOf(x) 500 | } 501 | 502 | // Deprecated: Use HeartbeatMsg.ProtoReflect.Descriptor instead. 503 | func (*HeartbeatMsg) Descriptor() ([]byte, []int) { 504 | return file_message_proto_rawDescGZIP(), []int{4} 505 | } 506 | 507 | // 上行消息 508 | type UpMsg struct { 509 | state protoimpl.MessageState 510 | sizeCache protoimpl.SizeCache 511 | unknownFields protoimpl.UnknownFields 512 | 513 | Msg *Message `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` // 消息内容 514 | ClientId uint64 `protobuf:"varint,2,opt,name=clientId,proto3" json:"clientId,omitempty"` // 保证上行消息可靠性 515 | } 516 | 517 | func (x *UpMsg) Reset() { 518 | *x = UpMsg{} 519 | if protoimpl.UnsafeEnabled { 520 | mi := &file_message_proto_msgTypes[5] 521 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 522 | ms.StoreMessageInfo(mi) 523 | } 524 | } 525 | 526 | func (x *UpMsg) String() string { 527 | return protoimpl.X.MessageStringOf(x) 528 | } 529 | 530 | func (*UpMsg) ProtoMessage() {} 531 | 532 | func (x *UpMsg) ProtoReflect() protoreflect.Message { 533 | mi := &file_message_proto_msgTypes[5] 534 | if protoimpl.UnsafeEnabled && x != nil { 535 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 536 | if ms.LoadMessageInfo() == nil { 537 | ms.StoreMessageInfo(mi) 538 | } 539 | return ms 540 | } 541 | return mi.MessageOf(x) 542 | } 543 | 544 | // Deprecated: Use UpMsg.ProtoReflect.Descriptor instead. 545 | func (*UpMsg) Descriptor() ([]byte, []int) { 546 | return file_message_proto_rawDescGZIP(), []int{5} 547 | } 548 | 549 | func (x *UpMsg) GetMsg() *Message { 550 | if x != nil { 551 | return x.Msg 552 | } 553 | return nil 554 | } 555 | 556 | func (x *UpMsg) GetClientId() uint64 { 557 | if x != nil { 558 | return x.ClientId 559 | } 560 | return 0 561 | } 562 | 563 | // 下行消息 564 | type PushMsg struct { 565 | state protoimpl.MessageState 566 | sizeCache protoimpl.SizeCache 567 | unknownFields protoimpl.UnknownFields 568 | 569 | Msg *Message `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` // 消息内容 570 | } 571 | 572 | func (x *PushMsg) Reset() { 573 | *x = PushMsg{} 574 | if protoimpl.UnsafeEnabled { 575 | mi := &file_message_proto_msgTypes[6] 576 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 577 | ms.StoreMessageInfo(mi) 578 | } 579 | } 580 | 581 | func (x *PushMsg) String() string { 582 | return protoimpl.X.MessageStringOf(x) 583 | } 584 | 585 | func (*PushMsg) ProtoMessage() {} 586 | 587 | func (x *PushMsg) ProtoReflect() protoreflect.Message { 588 | mi := &file_message_proto_msgTypes[6] 589 | if protoimpl.UnsafeEnabled && x != nil { 590 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 591 | if ms.LoadMessageInfo() == nil { 592 | ms.StoreMessageInfo(mi) 593 | } 594 | return ms 595 | } 596 | return mi.MessageOf(x) 597 | } 598 | 599 | // Deprecated: Use PushMsg.ProtoReflect.Descriptor instead. 600 | func (*PushMsg) Descriptor() ([]byte, []int) { 601 | return file_message_proto_rawDescGZIP(), []int{6} 602 | } 603 | 604 | func (x *PushMsg) GetMsg() *Message { 605 | if x != nil { 606 | return x.Msg 607 | } 608 | return nil 609 | } 610 | 611 | // 上行离线消息同步 612 | type SyncInputMsg struct { 613 | state protoimpl.MessageState 614 | sizeCache protoimpl.SizeCache 615 | unknownFields protoimpl.UnknownFields 616 | 617 | Seq uint64 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"` // 客户端已经同步的序列号 618 | } 619 | 620 | func (x *SyncInputMsg) Reset() { 621 | *x = SyncInputMsg{} 622 | if protoimpl.UnsafeEnabled { 623 | mi := &file_message_proto_msgTypes[7] 624 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 625 | ms.StoreMessageInfo(mi) 626 | } 627 | } 628 | 629 | func (x *SyncInputMsg) String() string { 630 | return protoimpl.X.MessageStringOf(x) 631 | } 632 | 633 | func (*SyncInputMsg) ProtoMessage() {} 634 | 635 | func (x *SyncInputMsg) ProtoReflect() protoreflect.Message { 636 | mi := &file_message_proto_msgTypes[7] 637 | if protoimpl.UnsafeEnabled && x != nil { 638 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 639 | if ms.LoadMessageInfo() == nil { 640 | ms.StoreMessageInfo(mi) 641 | } 642 | return ms 643 | } 644 | return mi.MessageOf(x) 645 | } 646 | 647 | // Deprecated: Use SyncInputMsg.ProtoReflect.Descriptor instead. 648 | func (*SyncInputMsg) Descriptor() ([]byte, []int) { 649 | return file_message_proto_rawDescGZIP(), []int{7} 650 | } 651 | 652 | func (x *SyncInputMsg) GetSeq() uint64 { 653 | if x != nil { 654 | return x.Seq 655 | } 656 | return 0 657 | } 658 | 659 | // 下行离线消息同步 660 | type SyncOutputMsg struct { 661 | state protoimpl.MessageState 662 | sizeCache protoimpl.SizeCache 663 | unknownFields protoimpl.UnknownFields 664 | 665 | Messages []*Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // 消息列表 666 | HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` // 是否还有更多数据 667 | } 668 | 669 | func (x *SyncOutputMsg) Reset() { 670 | *x = SyncOutputMsg{} 671 | if protoimpl.UnsafeEnabled { 672 | mi := &file_message_proto_msgTypes[8] 673 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 674 | ms.StoreMessageInfo(mi) 675 | } 676 | } 677 | 678 | func (x *SyncOutputMsg) String() string { 679 | return protoimpl.X.MessageStringOf(x) 680 | } 681 | 682 | func (*SyncOutputMsg) ProtoMessage() {} 683 | 684 | func (x *SyncOutputMsg) ProtoReflect() protoreflect.Message { 685 | mi := &file_message_proto_msgTypes[8] 686 | if protoimpl.UnsafeEnabled && x != nil { 687 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 688 | if ms.LoadMessageInfo() == nil { 689 | ms.StoreMessageInfo(mi) 690 | } 691 | return ms 692 | } 693 | return mi.MessageOf(x) 694 | } 695 | 696 | // Deprecated: Use SyncOutputMsg.ProtoReflect.Descriptor instead. 697 | func (*SyncOutputMsg) Descriptor() ([]byte, []int) { 698 | return file_message_proto_rawDescGZIP(), []int{8} 699 | } 700 | 701 | func (x *SyncOutputMsg) GetMessages() []*Message { 702 | if x != nil { 703 | return x.Messages 704 | } 705 | return nil 706 | } 707 | 708 | func (x *SyncOutputMsg) GetHasMore() bool { 709 | if x != nil { 710 | return x.HasMore 711 | } 712 | return false 713 | } 714 | 715 | // 消息投递 716 | // 上行、下行 717 | type Message struct { 718 | state protoimpl.MessageState 719 | sizeCache protoimpl.SizeCache 720 | unknownFields protoimpl.UnknownFields 721 | 722 | SessionType SessionType `protobuf:"varint,1,opt,name=session_type,json=sessionType,proto3,enum=pb.SessionType" json:"session_type,omitempty"` // 会话类型 单聊、群聊 723 | ReceiverId uint64 `protobuf:"varint,2,opt,name=receiver_id,json=receiverId,proto3" json:"receiver_id,omitempty"` // 接收者id 用户id/群组id 724 | SenderId uint64 `protobuf:"varint,3,opt,name=sender_id,json=senderId,proto3" json:"sender_id,omitempty"` // 发送者id 725 | MessageType MessageType `protobuf:"varint,4,opt,name=message_type,json=messageType,proto3,enum=pb.MessageType" json:"message_type,omitempty"` // 消息类型 文本、图片、语音 726 | Content []byte `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"` // 实际用户所发数据 727 | Seq uint64 `protobuf:"varint,6,opt,name=seq,proto3" json:"seq,omitempty"` // 客户端的最大消息同步序号 728 | SendTime int64 `protobuf:"varint,7,opt,name=send_time,json=sendTime,proto3" json:"send_time,omitempty"` // 消息发送时间戳,ms 729 | } 730 | 731 | func (x *Message) Reset() { 732 | *x = Message{} 733 | if protoimpl.UnsafeEnabled { 734 | mi := &file_message_proto_msgTypes[9] 735 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 736 | ms.StoreMessageInfo(mi) 737 | } 738 | } 739 | 740 | func (x *Message) String() string { 741 | return protoimpl.X.MessageStringOf(x) 742 | } 743 | 744 | func (*Message) ProtoMessage() {} 745 | 746 | func (x *Message) ProtoReflect() protoreflect.Message { 747 | mi := &file_message_proto_msgTypes[9] 748 | if protoimpl.UnsafeEnabled && x != nil { 749 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 750 | if ms.LoadMessageInfo() == nil { 751 | ms.StoreMessageInfo(mi) 752 | } 753 | return ms 754 | } 755 | return mi.MessageOf(x) 756 | } 757 | 758 | // Deprecated: Use Message.ProtoReflect.Descriptor instead. 759 | func (*Message) Descriptor() ([]byte, []int) { 760 | return file_message_proto_rawDescGZIP(), []int{9} 761 | } 762 | 763 | func (x *Message) GetSessionType() SessionType { 764 | if x != nil { 765 | return x.SessionType 766 | } 767 | return SessionType_ST_UnKnow 768 | } 769 | 770 | func (x *Message) GetReceiverId() uint64 { 771 | if x != nil { 772 | return x.ReceiverId 773 | } 774 | return 0 775 | } 776 | 777 | func (x *Message) GetSenderId() uint64 { 778 | if x != nil { 779 | return x.SenderId 780 | } 781 | return 0 782 | } 783 | 784 | func (x *Message) GetMessageType() MessageType { 785 | if x != nil { 786 | return x.MessageType 787 | } 788 | return MessageType_MT_UnKnow 789 | } 790 | 791 | func (x *Message) GetContent() []byte { 792 | if x != nil { 793 | return x.Content 794 | } 795 | return nil 796 | } 797 | 798 | func (x *Message) GetSeq() uint64 { 799 | if x != nil { 800 | return x.Seq 801 | } 802 | return 0 803 | } 804 | 805 | func (x *Message) GetSendTime() int64 { 806 | if x != nil { 807 | return x.SendTime 808 | } 809 | return 0 810 | } 811 | 812 | // ACK 回复 813 | // 根据顶层消息 type 解析得到 814 | // 客户端中发送场景: 815 | // 1. 客户端中收到 PushMsg 类型消息,向服务端回复 AT_Push 类型的 ACK,表明已收到 TODO 816 | // 服务端中发送场景: 817 | // 1. 服务端收到 CT_Login 消息,向客户端回复 AT_Login 类型的 ACK 818 | // 2. 服务端收到 UpMsg 类型消息, 向客户端回复 AT_Up 类型的 ACK 和 clientId,表明已收到该 ACK,无需超时重试 819 | type ACKMsg struct { 820 | state protoimpl.MessageState 821 | sizeCache protoimpl.SizeCache 822 | unknownFields protoimpl.UnknownFields 823 | 824 | Type ACKType `protobuf:"varint,1,opt,name=type,proto3,enum=pb.ACKType" json:"type,omitempty"` // 收到的是什么类型的 ACK 825 | ClientId uint64 `protobuf:"varint,2,opt,name=clientId,proto3" json:"clientId,omitempty"` 826 | Seq uint64 `protobuf:"varint,3,opt,name=seq,proto3" json:"seq,omitempty"` // 上行消息推送时回复最新 seq 827 | } 828 | 829 | func (x *ACKMsg) Reset() { 830 | *x = ACKMsg{} 831 | if protoimpl.UnsafeEnabled { 832 | mi := &file_message_proto_msgTypes[10] 833 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 834 | ms.StoreMessageInfo(mi) 835 | } 836 | } 837 | 838 | func (x *ACKMsg) String() string { 839 | return protoimpl.X.MessageStringOf(x) 840 | } 841 | 842 | func (*ACKMsg) ProtoMessage() {} 843 | 844 | func (x *ACKMsg) ProtoReflect() protoreflect.Message { 845 | mi := &file_message_proto_msgTypes[10] 846 | if protoimpl.UnsafeEnabled && x != nil { 847 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 848 | if ms.LoadMessageInfo() == nil { 849 | ms.StoreMessageInfo(mi) 850 | } 851 | return ms 852 | } 853 | return mi.MessageOf(x) 854 | } 855 | 856 | // Deprecated: Use ACKMsg.ProtoReflect.Descriptor instead. 857 | func (*ACKMsg) Descriptor() ([]byte, []int) { 858 | return file_message_proto_rawDescGZIP(), []int{10} 859 | } 860 | 861 | func (x *ACKMsg) GetType() ACKType { 862 | if x != nil { 863 | return x.Type 864 | } 865 | return ACKType_AT_UnKnow 866 | } 867 | 868 | func (x *ACKMsg) GetClientId() uint64 { 869 | if x != nil { 870 | return x.ClientId 871 | } 872 | return 0 873 | } 874 | 875 | func (x *ACKMsg) GetSeq() uint64 { 876 | if x != nil { 877 | return x.Seq 878 | } 879 | return 0 880 | } 881 | 882 | var File_message_proto protoreflect.FileDescriptor 883 | 884 | var file_message_proto_rawDesc = []byte{ 885 | 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 886 | 0x02, 0x70, 0x62, 0x22, 0x3c, 0x0a, 0x05, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x04, 887 | 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 888 | 0x43, 0x6d, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 889 | 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 890 | 0x61, 0x22, 0x6b, 0x0a, 0x06, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x04, 0x74, 891 | 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x43, 892 | 0x6d, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 893 | 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 894 | 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x64, 0x65, 0x4d, 0x73, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 895 | 0x09, 0x52, 0x07, 0x43, 0x6f, 0x64, 0x65, 0x4d, 0x73, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 896 | 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x27, 897 | 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x18, 0x0a, 898 | 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x07, 899 | 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x22, 0x20, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 900 | 0x4d, 0x73, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 901 | 0x28, 0x0c, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x48, 0x65, 0x61, 902 | 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4d, 0x73, 0x67, 0x22, 0x42, 0x0a, 0x05, 0x55, 0x70, 0x4d, 903 | 0x73, 0x67, 0x12, 0x1d, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 904 | 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x03, 0x6d, 0x73, 905 | 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 906 | 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x28, 0x0a, 907 | 0x07, 0x50, 0x75, 0x73, 0x68, 0x4d, 0x73, 0x67, 0x12, 0x1d, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 908 | 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 909 | 0x67, 0x65, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x20, 0x0a, 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x49, 910 | 0x6e, 0x70, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x71, 0x18, 0x01, 911 | 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 0x65, 0x71, 0x22, 0x53, 0x0a, 0x0d, 0x53, 0x79, 0x6e, 912 | 0x63, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x27, 0x0a, 0x08, 0x6d, 0x65, 913 | 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 914 | 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 915 | 0x67, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 916 | 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0xf8, 917 | 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, 0x0c, 0x73, 0x65, 918 | 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 919 | 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 920 | 0x65, 0x52, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 921 | 0x0a, 0x0b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 922 | 0x01, 0x28, 0x04, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, 923 | 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 924 | 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0c, 925 | 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 926 | 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 927 | 0x79, 0x70, 0x65, 0x52, 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 928 | 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 929 | 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 930 | 0x71, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 0x65, 0x71, 0x12, 0x1b, 0x0a, 0x09, 931 | 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 932 | 0x08, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x57, 0x0a, 0x06, 0x41, 0x43, 0x4b, 933 | 0x4d, 0x73, 0x67, 0x12, 0x1f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 934 | 0x0e, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x43, 0x4b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 935 | 0x74, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 936 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 937 | 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x71, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 938 | 0x65, 0x71, 0x2a, 0x39, 0x0a, 0x0b, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 939 | 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x54, 0x5f, 0x55, 0x6e, 0x4b, 0x6e, 0x6f, 0x77, 0x10, 0x00, 940 | 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x54, 0x5f, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x10, 0x01, 0x12, 941 | 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x5f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x10, 0x02, 0x2a, 0x47, 0x0a, 942 | 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 943 | 0x4d, 0x54, 0x5f, 0x55, 0x6e, 0x4b, 0x6e, 0x6f, 0x77, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 944 | 0x54, 0x5f, 0x54, 0x65, 0x78, 0x74, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4d, 0x54, 0x5f, 0x50, 945 | 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x54, 0x5f, 0x56, 946 | 0x6f, 0x69, 0x63, 0x65, 0x10, 0x03, 0x2a, 0x3e, 0x0a, 0x07, 0x41, 0x43, 0x4b, 0x54, 0x79, 0x70, 947 | 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x41, 0x54, 0x5f, 0x55, 0x6e, 0x4b, 0x6e, 0x6f, 0x77, 0x10, 0x00, 948 | 0x12, 0x09, 0x0a, 0x05, 0x41, 0x54, 0x5f, 0x55, 0x70, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x41, 949 | 0x54, 0x5f, 0x50, 0x75, 0x73, 0x68, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x54, 0x5f, 0x4c, 950 | 0x6f, 0x67, 0x69, 0x6e, 0x10, 0x03, 0x2a, 0x61, 0x0a, 0x07, 0x43, 0x6d, 0x64, 0x54, 0x79, 0x70, 951 | 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x54, 0x5f, 0x55, 0x6e, 0x4b, 0x6e, 0x6f, 0x77, 0x10, 0x00, 952 | 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x54, 0x5f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x10, 953 | 0x0a, 0x0c, 0x43, 0x54, 0x5f, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x10, 0x02, 954 | 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x54, 0x5f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x10, 0x03, 955 | 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x54, 0x5f, 0x41, 0x43, 0x4b, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 956 | 0x43, 0x54, 0x5f, 0x53, 0x79, 0x6e, 0x63, 0x10, 0x05, 0x42, 0x18, 0x5a, 0x16, 0x47, 0x6f, 0x43, 957 | 0x68, 0x61, 0x74, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 958 | 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 959 | } 960 | 961 | var ( 962 | file_message_proto_rawDescOnce sync.Once 963 | file_message_proto_rawDescData = file_message_proto_rawDesc 964 | ) 965 | 966 | func file_message_proto_rawDescGZIP() []byte { 967 | file_message_proto_rawDescOnce.Do(func() { 968 | file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) 969 | }) 970 | return file_message_proto_rawDescData 971 | } 972 | 973 | var file_message_proto_enumTypes = make([]protoimpl.EnumInfo, 4) 974 | var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 11) 975 | var file_message_proto_goTypes = []interface{}{ 976 | (SessionType)(0), // 0: pb.SessionType 977 | (MessageType)(0), // 1: pb.MessageType 978 | (ACKType)(0), // 2: pb.ACKType 979 | (CmdType)(0), // 3: pb.CmdType 980 | (*Input)(nil), // 4: pb.Input 981 | (*Output)(nil), // 5: pb.Output 982 | (*OutputBatch)(nil), // 6: pb.OutputBatch 983 | (*LoginMsg)(nil), // 7: pb.LoginMsg 984 | (*HeartbeatMsg)(nil), // 8: pb.HeartbeatMsg 985 | (*UpMsg)(nil), // 9: pb.UpMsg 986 | (*PushMsg)(nil), // 10: pb.PushMsg 987 | (*SyncInputMsg)(nil), // 11: pb.SyncInputMsg 988 | (*SyncOutputMsg)(nil), // 12: pb.SyncOutputMsg 989 | (*Message)(nil), // 13: pb.Message 990 | (*ACKMsg)(nil), // 14: pb.ACKMsg 991 | } 992 | var file_message_proto_depIdxs = []int32{ 993 | 3, // 0: pb.Input.type:type_name -> pb.CmdType 994 | 3, // 1: pb.Output.type:type_name -> pb.CmdType 995 | 13, // 2: pb.UpMsg.msg:type_name -> pb.Message 996 | 13, // 3: pb.PushMsg.msg:type_name -> pb.Message 997 | 13, // 4: pb.SyncOutputMsg.messages:type_name -> pb.Message 998 | 0, // 5: pb.Message.session_type:type_name -> pb.SessionType 999 | 1, // 6: pb.Message.message_type:type_name -> pb.MessageType 1000 | 2, // 7: pb.ACKMsg.type:type_name -> pb.ACKType 1001 | 8, // [8:8] is the sub-list for method output_type 1002 | 8, // [8:8] is the sub-list for method input_type 1003 | 8, // [8:8] is the sub-list for extension type_name 1004 | 8, // [8:8] is the sub-list for extension extendee 1005 | 0, // [0:8] is the sub-list for field type_name 1006 | } 1007 | 1008 | func init() { file_message_proto_init() } 1009 | func file_message_proto_init() { 1010 | if File_message_proto != nil { 1011 | return 1012 | } 1013 | if !protoimpl.UnsafeEnabled { 1014 | file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 1015 | switch v := v.(*Input); i { 1016 | case 0: 1017 | return &v.state 1018 | case 1: 1019 | return &v.sizeCache 1020 | case 2: 1021 | return &v.unknownFields 1022 | default: 1023 | return nil 1024 | } 1025 | } 1026 | file_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 1027 | switch v := v.(*Output); i { 1028 | case 0: 1029 | return &v.state 1030 | case 1: 1031 | return &v.sizeCache 1032 | case 2: 1033 | return &v.unknownFields 1034 | default: 1035 | return nil 1036 | } 1037 | } 1038 | file_message_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 1039 | switch v := v.(*OutputBatch); i { 1040 | case 0: 1041 | return &v.state 1042 | case 1: 1043 | return &v.sizeCache 1044 | case 2: 1045 | return &v.unknownFields 1046 | default: 1047 | return nil 1048 | } 1049 | } 1050 | file_message_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 1051 | switch v := v.(*LoginMsg); i { 1052 | case 0: 1053 | return &v.state 1054 | case 1: 1055 | return &v.sizeCache 1056 | case 2: 1057 | return &v.unknownFields 1058 | default: 1059 | return nil 1060 | } 1061 | } 1062 | file_message_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 1063 | switch v := v.(*HeartbeatMsg); i { 1064 | case 0: 1065 | return &v.state 1066 | case 1: 1067 | return &v.sizeCache 1068 | case 2: 1069 | return &v.unknownFields 1070 | default: 1071 | return nil 1072 | } 1073 | } 1074 | file_message_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 1075 | switch v := v.(*UpMsg); i { 1076 | case 0: 1077 | return &v.state 1078 | case 1: 1079 | return &v.sizeCache 1080 | case 2: 1081 | return &v.unknownFields 1082 | default: 1083 | return nil 1084 | } 1085 | } 1086 | file_message_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 1087 | switch v := v.(*PushMsg); i { 1088 | case 0: 1089 | return &v.state 1090 | case 1: 1091 | return &v.sizeCache 1092 | case 2: 1093 | return &v.unknownFields 1094 | default: 1095 | return nil 1096 | } 1097 | } 1098 | file_message_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 1099 | switch v := v.(*SyncInputMsg); i { 1100 | case 0: 1101 | return &v.state 1102 | case 1: 1103 | return &v.sizeCache 1104 | case 2: 1105 | return &v.unknownFields 1106 | default: 1107 | return nil 1108 | } 1109 | } 1110 | file_message_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 1111 | switch v := v.(*SyncOutputMsg); i { 1112 | case 0: 1113 | return &v.state 1114 | case 1: 1115 | return &v.sizeCache 1116 | case 2: 1117 | return &v.unknownFields 1118 | default: 1119 | return nil 1120 | } 1121 | } 1122 | file_message_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { 1123 | switch v := v.(*Message); i { 1124 | case 0: 1125 | return &v.state 1126 | case 1: 1127 | return &v.sizeCache 1128 | case 2: 1129 | return &v.unknownFields 1130 | default: 1131 | return nil 1132 | } 1133 | } 1134 | file_message_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { 1135 | switch v := v.(*ACKMsg); i { 1136 | case 0: 1137 | return &v.state 1138 | case 1: 1139 | return &v.sizeCache 1140 | case 2: 1141 | return &v.unknownFields 1142 | default: 1143 | return nil 1144 | } 1145 | } 1146 | } 1147 | type x struct{} 1148 | out := protoimpl.TypeBuilder{ 1149 | File: protoimpl.DescBuilder{ 1150 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 1151 | RawDescriptor: file_message_proto_rawDesc, 1152 | NumEnums: 4, 1153 | NumMessages: 11, 1154 | NumExtensions: 0, 1155 | NumServices: 0, 1156 | }, 1157 | GoTypes: file_message_proto_goTypes, 1158 | DependencyIndexes: file_message_proto_depIdxs, 1159 | EnumInfos: file_message_proto_enumTypes, 1160 | MessageInfos: file_message_proto_msgTypes, 1161 | }.Build() 1162 | File_message_proto = out.File 1163 | file_message_proto_rawDesc = nil 1164 | file_message_proto_goTypes = nil 1165 | file_message_proto_depIdxs = nil 1166 | } 1167 | -------------------------------------------------------------------------------- /pkg/protocol/pb/mq_msg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.20.1 5 | // source: mq_msg.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type MQMessages struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Messages []*MQMessage `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` 30 | } 31 | 32 | func (x *MQMessages) Reset() { 33 | *x = MQMessages{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_mq_msg_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *MQMessages) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*MQMessages) ProtoMessage() {} 46 | 47 | func (x *MQMessages) ProtoReflect() protoreflect.Message { 48 | mi := &file_mq_msg_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use MQMessages.ProtoReflect.Descriptor instead. 60 | func (*MQMessages) Descriptor() ([]byte, []int) { 61 | return file_mq_msg_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *MQMessages) GetMessages() []*MQMessage { 65 | if x != nil { 66 | return x.Messages 67 | } 68 | return nil 69 | } 70 | 71 | type MQMessage struct { 72 | state protoimpl.MessageState 73 | sizeCache protoimpl.SizeCache 74 | unknownFields protoimpl.UnknownFields 75 | 76 | Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 77 | UserId uint64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` 78 | SenderId uint64 `protobuf:"varint,3,opt,name=sender_id,json=senderId,proto3" json:"sender_id,omitempty"` 79 | SessionType int32 `protobuf:"varint,4,opt,name=session_type,json=sessionType,proto3" json:"session_type,omitempty"` 80 | ReceiverId uint64 `protobuf:"varint,5,opt,name=receiver_id,json=receiverId,proto3" json:"receiver_id,omitempty"` 81 | MessageType int32 `protobuf:"varint,6,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` 82 | Content []byte `protobuf:"bytes,7,opt,name=content,proto3" json:"content,omitempty"` 83 | Seq uint64 `protobuf:"varint,8,opt,name=seq,proto3" json:"seq,omitempty"` 84 | SendTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=send_time,json=sendTime,proto3" json:"send_time,omitempty"` 85 | CreateTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` 86 | UpdateTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` 87 | } 88 | 89 | func (x *MQMessage) Reset() { 90 | *x = MQMessage{} 91 | if protoimpl.UnsafeEnabled { 92 | mi := &file_mq_msg_proto_msgTypes[1] 93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 94 | ms.StoreMessageInfo(mi) 95 | } 96 | } 97 | 98 | func (x *MQMessage) String() string { 99 | return protoimpl.X.MessageStringOf(x) 100 | } 101 | 102 | func (*MQMessage) ProtoMessage() {} 103 | 104 | func (x *MQMessage) ProtoReflect() protoreflect.Message { 105 | mi := &file_mq_msg_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 MQMessage.ProtoReflect.Descriptor instead. 117 | func (*MQMessage) Descriptor() ([]byte, []int) { 118 | return file_mq_msg_proto_rawDescGZIP(), []int{1} 119 | } 120 | 121 | func (x *MQMessage) GetId() uint64 { 122 | if x != nil { 123 | return x.Id 124 | } 125 | return 0 126 | } 127 | 128 | func (x *MQMessage) GetUserId() uint64 { 129 | if x != nil { 130 | return x.UserId 131 | } 132 | return 0 133 | } 134 | 135 | func (x *MQMessage) GetSenderId() uint64 { 136 | if x != nil { 137 | return x.SenderId 138 | } 139 | return 0 140 | } 141 | 142 | func (x *MQMessage) GetSessionType() int32 { 143 | if x != nil { 144 | return x.SessionType 145 | } 146 | return 0 147 | } 148 | 149 | func (x *MQMessage) GetReceiverId() uint64 { 150 | if x != nil { 151 | return x.ReceiverId 152 | } 153 | return 0 154 | } 155 | 156 | func (x *MQMessage) GetMessageType() int32 { 157 | if x != nil { 158 | return x.MessageType 159 | } 160 | return 0 161 | } 162 | 163 | func (x *MQMessage) GetContent() []byte { 164 | if x != nil { 165 | return x.Content 166 | } 167 | return nil 168 | } 169 | 170 | func (x *MQMessage) GetSeq() uint64 { 171 | if x != nil { 172 | return x.Seq 173 | } 174 | return 0 175 | } 176 | 177 | func (x *MQMessage) GetSendTime() *timestamppb.Timestamp { 178 | if x != nil { 179 | return x.SendTime 180 | } 181 | return nil 182 | } 183 | 184 | func (x *MQMessage) GetCreateTime() *timestamppb.Timestamp { 185 | if x != nil { 186 | return x.CreateTime 187 | } 188 | return nil 189 | } 190 | 191 | func (x *MQMessage) GetUpdateTime() *timestamppb.Timestamp { 192 | if x != nil { 193 | return x.UpdateTime 194 | } 195 | return nil 196 | } 197 | 198 | var File_mq_msg_proto protoreflect.FileDescriptor 199 | 200 | var file_mq_msg_proto_rawDesc = []byte{ 201 | 0x0a, 0x0c, 0x6d, 0x71, 0x5f, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 202 | 0x70, 0x62, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 203 | 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 204 | 0x6f, 0x74, 0x6f, 0x22, 0x37, 0x0a, 0x0a, 0x4d, 0x51, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 205 | 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 206 | 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x51, 0x4d, 0x65, 0x73, 0x73, 0x61, 207 | 0x67, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x97, 0x03, 0x0a, 208 | 0x09, 0x4d, 0x51, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 209 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 210 | 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x75, 0x73, 0x65, 211 | 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 212 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x49, 0x64, 213 | 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 214 | 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 215 | 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 216 | 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 217 | 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 218 | 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6d, 0x65, 0x73, 0x73, 219 | 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 220 | 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 221 | 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x71, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 222 | 0x73, 0x65, 0x71, 0x12, 0x37, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 223 | 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 224 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 225 | 0x6d, 0x70, 0x52, 0x08, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x0b, 226 | 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 227 | 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 228 | 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 229 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 230 | 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 231 | 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 232 | 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 233 | 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x18, 0x5a, 0x16, 0x47, 0x6f, 0x43, 0x68, 0x61, 0x74, 234 | 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x70, 0x62, 235 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 236 | } 237 | 238 | var ( 239 | file_mq_msg_proto_rawDescOnce sync.Once 240 | file_mq_msg_proto_rawDescData = file_mq_msg_proto_rawDesc 241 | ) 242 | 243 | func file_mq_msg_proto_rawDescGZIP() []byte { 244 | file_mq_msg_proto_rawDescOnce.Do(func() { 245 | file_mq_msg_proto_rawDescData = protoimpl.X.CompressGZIP(file_mq_msg_proto_rawDescData) 246 | }) 247 | return file_mq_msg_proto_rawDescData 248 | } 249 | 250 | var file_mq_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 251 | var file_mq_msg_proto_goTypes = []interface{}{ 252 | (*MQMessages)(nil), // 0: pb.MQMessages 253 | (*MQMessage)(nil), // 1: pb.MQMessage 254 | (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp 255 | } 256 | var file_mq_msg_proto_depIdxs = []int32{ 257 | 1, // 0: pb.MQMessages.messages:type_name -> pb.MQMessage 258 | 2, // 1: pb.MQMessage.send_time:type_name -> google.protobuf.Timestamp 259 | 2, // 2: pb.MQMessage.create_time:type_name -> google.protobuf.Timestamp 260 | 2, // 3: pb.MQMessage.update_time:type_name -> google.protobuf.Timestamp 261 | 4, // [4:4] is the sub-list for method output_type 262 | 4, // [4:4] is the sub-list for method input_type 263 | 4, // [4:4] is the sub-list for extension type_name 264 | 4, // [4:4] is the sub-list for extension extendee 265 | 0, // [0:4] is the sub-list for field type_name 266 | } 267 | 268 | func init() { file_mq_msg_proto_init() } 269 | func file_mq_msg_proto_init() { 270 | if File_mq_msg_proto != nil { 271 | return 272 | } 273 | if !protoimpl.UnsafeEnabled { 274 | file_mq_msg_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 275 | switch v := v.(*MQMessages); i { 276 | case 0: 277 | return &v.state 278 | case 1: 279 | return &v.sizeCache 280 | case 2: 281 | return &v.unknownFields 282 | default: 283 | return nil 284 | } 285 | } 286 | file_mq_msg_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 287 | switch v := v.(*MQMessage); i { 288 | case 0: 289 | return &v.state 290 | case 1: 291 | return &v.sizeCache 292 | case 2: 293 | return &v.unknownFields 294 | default: 295 | return nil 296 | } 297 | } 298 | } 299 | type x struct{} 300 | out := protoimpl.TypeBuilder{ 301 | File: protoimpl.DescBuilder{ 302 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 303 | RawDescriptor: file_mq_msg_proto_rawDesc, 304 | NumEnums: 0, 305 | NumMessages: 2, 306 | NumExtensions: 0, 307 | NumServices: 0, 308 | }, 309 | GoTypes: file_mq_msg_proto_goTypes, 310 | DependencyIndexes: file_mq_msg_proto_depIdxs, 311 | MessageInfos: file_mq_msg_proto_msgTypes, 312 | }.Build() 313 | File_mq_msg_proto = out.File 314 | file_mq_msg_proto_rawDesc = nil 315 | file_mq_msg_proto_goTypes = nil 316 | file_mq_msg_proto_depIdxs = nil 317 | } 318 | -------------------------------------------------------------------------------- /pkg/protocol/proto/conn.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pb; 3 | option go_package = "GoChat/pkg/protocol/pb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service Connect { 8 | // 私聊消息投递 9 | rpc DeliverMessage (DeliverMessageReq) returns (google.protobuf.Empty); 10 | // 群聊消息投递 11 | rpc DeliverMessageAll(DeliverMessageAllReq) returns (google.protobuf.Empty); 12 | } 13 | 14 | message DeliverMessageReq { 15 | uint64 receiver_id = 1; // 消息接收者 16 | bytes data = 2; // 要投递的消息 17 | } 18 | 19 | message DeliverMessageAllReq{ 20 | map receiver_id_2_data = 1; // 消息接受者到要投递的消息的映射 21 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pb; 3 | option go_package = "GoChat/pkg/protocol/pb"; 4 | 5 | // 会话类型 6 | enum SessionType {// 枚举聊天类型 7 | ST_UnKnow = 0; // 未知 8 | ST_Single = 1; // 单聊 9 | ST_Group = 2; // 群聊 10 | } 11 | 12 | // 用户所发送内容的消息类型 13 | enum MessageType {// 枚举发送的消息类型 14 | MT_UnKnow = 0; // 未知 15 | MT_Text = 1; // 文本类型消息 16 | MT_Picture = 2; // 图片类型消息 17 | MT_Voice = 3; // 语音类型消息 18 | } 19 | 20 | // ACK 消息类型,先根据 Input/Output 的 type 解析出是 ACK,再根据 ACKType 判断是 ACK 的是什么消息 21 | enum ACKType { 22 | AT_UnKnow = 0; // 未知 23 | AT_Up = 1 ; // 服务端回复客户端发来的消息 24 | AT_Push = 2; // 客户端回复服务端发来的消息 25 | AT_Login = 3; // 登录 26 | } 27 | 28 | // 所有 websocket 的消息类型 29 | enum CmdType {// 枚举消息类型 30 | CT_UnKnow = 0; // 未知 31 | CT_Login = 1; // 连接注册,客户端向服务端发送,建立连接 32 | CT_Heartbeat = 2; // 心跳,客户端向服务端发送,连接保活 33 | CT_Message = 3; // 消息投递,可能是服务端发给客户端,也可能是客户端发给服务端 34 | CT_ACK = 4; // ACK 35 | CT_Sync = 5; // 离线消息同步 36 | } 37 | 38 | // 上行消息(客户端发送给服务端)顶层消息 39 | // 使用: 40 | // 客户端发送前:先组装出下层消息例如 HeartBeatMsg,序列化作为 Input 的 data 值,再填写 type 值,序列化 Input 发送给服务端 41 | // 服务端收到后:反序列化成 Input,根据 type 值调用不同类型 handler,在 handler 中将 data 解析成其他例如 LoginMsg 类型消息,再做处理 42 | message Input { 43 | CmdType type = 1; // 消息类型,根据不同消息类型,可以将 data 解析成下面其他类型 44 | bytes data = 2; // 数据 45 | } 46 | 47 | // 下行消息(服务端发送给客户端)顶层消息 48 | // 使用: 49 | // 服务端发送前:组装出下层消息例如 Message,序列化作为 Output 的 data 值,再填写其他值,序列化 Output 发送给客户端 50 | // 客户端收到后:反序列化成 Output,根据 type 值调用不同类型 handler,在 handler 中将 data 解析成其他例如 Message 类型消息,再做处理 51 | message Output { 52 | CmdType type = 1; // 消息类型,根据不同的消息类型,可以将 data 解析成下面其他类型 53 | int32 code = 2; // 错误码 54 | string CodeMsg = 3; // 错误码信息 55 | bytes data = 4; // 数据 56 | } 57 | 58 | // 下行消息批处理 59 | message OutputBatch { 60 | repeated bytes outputs = 1; 61 | } 62 | 63 | // 登录 64 | message LoginMsg { 65 | bytes token = 1; // token 66 | } 67 | 68 | // 心跳 69 | message HeartbeatMsg {} 70 | 71 | // 上行消息 72 | message UpMsg { 73 | Message msg = 1; // 消息内容 74 | uint64 clientId = 2; // 保证上行消息可靠性 75 | } 76 | 77 | // 下行消息 78 | message PushMsg { 79 | Message msg = 1; // 消息内容 80 | } 81 | 82 | // 上行离线消息同步 83 | message SyncInputMsg { 84 | uint64 seq = 1; // 客户端已经同步的序列号 85 | } 86 | 87 | // 下行离线消息同步 88 | message SyncOutputMsg { 89 | repeated Message messages = 1; // 消息列表 90 | bool has_more = 2; // 是否还有更多数据 91 | } 92 | 93 | // 消息投递 94 | // 上行、下行 95 | message Message { 96 | SessionType session_type = 1; // 会话类型 单聊、群聊 97 | uint64 receiver_id = 2; // 接收者id 用户id/群组id 98 | uint64 sender_id = 3; // 发送者id 99 | MessageType message_type = 4; // 消息类型 文本、图片、语音 100 | bytes content = 5; // 实际用户所发数据 101 | uint64 seq = 6; // 客户端的最大消息同步序号 102 | int64 send_time = 7; // 消息发送时间戳,ms 103 | } 104 | 105 | // ACK 回复 106 | // 根据顶层消息 type 解析得到 107 | // 客户端中发送场景: 108 | // 1. 客户端中收到 PushMsg 类型消息,向服务端回复 AT_Push 类型的 ACK,表明已收到 TODO 109 | // 服务端中发送场景: 110 | // 1. 服务端收到 CT_Login 消息,向客户端回复 AT_Login 类型的 ACK 111 | // 2. 服务端收到 UpMsg 类型消息, 向客户端回复 AT_Up 类型的 ACK 和 clientId,表明已收到该 ACK,无需超时重试 112 | message ACKMsg { 113 | ACKType type = 1; // 收到的是什么类型的 ACK 114 | uint64 clientId = 2; 115 | uint64 seq = 3; // 上行消息推送时回复最新 seq 116 | } 117 | 118 | -------------------------------------------------------------------------------- /pkg/protocol/proto/mq_msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pb; 3 | option go_package = "GoChat/pkg/protocol/pb"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message MQMessages { 8 | repeated MQMessage messages = 1; 9 | } 10 | 11 | message MQMessage { 12 | uint64 id = 1; 13 | uint64 user_id = 2; 14 | uint64 sender_id = 3; 15 | int32 session_type = 4; 16 | uint64 receiver_id = 5; 17 | int32 message_type = 6; 18 | bytes content = 7; 19 | uint64 seq = 8; 20 | google.protobuf.Timestamp send_time = 9; 21 | google.protobuf.Timestamp create_time = 10; 22 | google.protobuf.Timestamp update_time = 11; 23 | } 24 | -------------------------------------------------------------------------------- /pkg/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "GoChat/pkg/protocol/pb" 5 | "fmt" 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | var ( 10 | ConnServerClient pb.ConnectClient 11 | ) 12 | 13 | // GetServerClient 获取 grpc 连接 14 | func GetServerClient(addr string) pb.ConnectClient { 15 | client, err := grpc.Dial(addr, grpc.WithInsecure()) 16 | if err != nil { 17 | fmt.Println("grpc client Dial err, err:", err) 18 | panic(err) 19 | } 20 | ConnServerClient = pb.NewConnectClient(client) 21 | return ConnServerClient 22 | } 23 | -------------------------------------------------------------------------------- /pkg/util/md5.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "GoChat/config" 5 | "crypto/md5" 6 | "fmt" 7 | ) 8 | 9 | // GetMD5 加盐生成 md5 10 | func GetMD5(s string) string { 11 | return fmt.Sprintf("%x", md5.Sum([]byte(s+config.GlobalConfig.App.Salt))) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/util/panic.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // RecoverPanic 恢复panic 9 | func RecoverPanic() { 10 | err := recover() 11 | if err != nil { 12 | fmt.Println("panic ", err, "stack:", GetStackInfo()) 13 | } 14 | } 15 | 16 | // GetStackInfo 获取Panic堆栈信息 17 | func GetStackInfo() string { 18 | buf := make([]byte, 4096) 19 | n := runtime.Stack(buf, false) 20 | return fmt.Sprintf("%s", buf[:n]) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/util/strconv.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | // StrToUint64 str -> uint64 6 | func StrToUint64(str string) uint64 { 7 | i, _ := strconv.ParseUint(str, 10, 64) 8 | return i 9 | } 10 | 11 | // Uint64ToStr uint64 -> str 12 | func Uint64ToStr(num uint64) string { 13 | return strconv.FormatUint(num, 10) 14 | } 15 | 16 | // Int64ToStr int64 -> str 17 | func Int64ToStr(num int64) string { 18 | return strconv.FormatInt(num, 10) 19 | } 20 | 21 | // StrToInt64 str -> int64 22 | func StrToInt64(str string) int64 { 23 | i, _ := strconv.ParseInt(str, 10, 64) 24 | return i 25 | } 26 | -------------------------------------------------------------------------------- /pkg/util/token.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "GoChat/config" 5 | "fmt" 6 | "github.com/golang-jwt/jwt/v4" 7 | "time" 8 | ) 9 | 10 | type UserClaims struct { 11 | UserId uint64 `json:"user_id"` 12 | jwt.RegisteredClaims 13 | } 14 | 15 | // GenerateToken 16 | // 生成 token 17 | func GenerateToken(userId uint64) (string, error) { 18 | UserClaim := &UserClaims{ 19 | UserId: userId, 20 | RegisteredClaims: jwt.RegisteredClaims{ 21 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(config.GlobalConfig.JWT.ExpireTime))), 22 | }, 23 | } 24 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim) 25 | tokenString, err := token.SignedString([]byte(config.GlobalConfig.JWT.SignKey)) 26 | if err != nil { 27 | return "", err 28 | } 29 | return tokenString, nil 30 | } 31 | 32 | // AnalyseToken 33 | // 解析 token 34 | func AnalyseToken(tokenString string) (*UserClaims, error) { 35 | userClaim := new(UserClaims) 36 | claims, err := jwt.ParseWithClaims(tokenString, userClaim, func(token *jwt.Token) (interface{}, error) { 37 | return []byte(config.GlobalConfig.JWT.SignKey), nil 38 | }) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if !claims.Valid { 43 | return nil, fmt.Errorf("analyse Token Error:%v", err) 44 | } 45 | return userClaim, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/util/uid.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "GoChat/pkg/db" 5 | "database/sql" 6 | "gorm.io/gorm" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | UidStep = 1000 12 | ) 13 | 14 | var ( 15 | UidGen = NewGeneratorUid() 16 | ) 17 | 18 | // uid 发号器 19 | type uidGenerator struct { 20 | batchUidMap map[string]*uid // 存在发号器中的一批 uid,其中 k 为 businessId,v 为 cur_seq 21 | mu sync.Mutex 22 | } 23 | 24 | func NewGeneratorUid() *uidGenerator { 25 | return &uidGenerator{ 26 | batchUidMap: make(map[string]*uid), 27 | } 28 | } 29 | 30 | // GetNextId 获取下一个 id 31 | func (u *uidGenerator) GetNextId(businessId string) (uint64, error) { 32 | u.mu.Lock() 33 | defer u.mu.Unlock() 34 | if uid, ok := u.batchUidMap[businessId]; ok { 35 | return uid.nextId() 36 | } 37 | uid := newUid(businessId) 38 | u.batchUidMap[businessId] = uid 39 | return uid.nextId() 40 | } 41 | 42 | // GetNextIds 获取一批 businessId 43 | func (u *uidGenerator) GetNextIds(businessIds []string) ([]uint64, error) { 44 | result := make([]uint64, 0, len(businessIds)) 45 | for _, businessId := range businessIds { 46 | id, err := u.GetNextId(businessId) 47 | if err != nil { 48 | return nil, err 49 | } 50 | result = append(result, id) 51 | } 52 | return result, nil 53 | } 54 | 55 | type uid struct { 56 | businessId string // 业务id 57 | curId uint64 // 当前分配的 id 58 | maxId uint64 // 当前号段最大 id 59 | step int // 每次分配出的号段步长 60 | mu sync.Mutex 61 | } 62 | 63 | func newUid(businessId string) *uid { 64 | id := &uid{ 65 | businessId: businessId, 66 | curId: 0, 67 | maxId: 0, 68 | step: UidStep, 69 | } 70 | return id 71 | } 72 | 73 | // 假设 step = 1000 时, 74 | // 首次获取,cur_id = 1, max_id = 1000,取出号段 [1, 1000] 75 | // 再次获取,cur_id = 1001, max_id = 2000,取出号段 [1001, 2000] 76 | func (u *uid) nextId() (uint64, error) { 77 | // 加锁保证并发安全 78 | u.mu.Lock() 79 | defer u.mu.Unlock() 80 | 81 | // 判断是否需要更新 ID 段 82 | if u.curId == u.maxId { 83 | err := u.getFromDB() 84 | if err != nil { 85 | return 0, err 86 | } 87 | } 88 | 89 | u.curId++ 90 | return u.curId, nil 91 | } 92 | 93 | // 从数据库拉取id段 94 | // 如果存在,cur_id 从 max_id 开始,max_id = max_id + step,分配出去 [step, max_id + step) 95 | func (u *uid) getFromDB() error { 96 | var ( 97 | maxId uint64 98 | step int 99 | ) 100 | err := db.DB.Transaction(func(tx *gorm.DB) error { 101 | // 查询 102 | err := tx.Raw("select max_id, step from uid where business_id = ? for update", u.businessId).Row().Scan(&maxId, &step) 103 | if err != nil && err != sql.ErrNoRows { 104 | return err 105 | } 106 | // 不存在就插入 107 | if err == sql.ErrNoRows { 108 | err = tx.Exec("insert into uid(business_id, max_id, step) values(?,?,?)", u.businessId, u.maxId, u.step).Error 109 | if err != nil { 110 | return err 111 | } 112 | 113 | } else { 114 | // 存在就更新 115 | err = tx.Exec("update uid set max_id = max_id + step where business_id = ?", u.businessId).Error 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | return nil 121 | }) 122 | if err != nil { 123 | return err 124 | } 125 | if maxId != 0 { 126 | // 如果已存在,cur_id = max_id 127 | u.curId = maxId 128 | } 129 | u.maxId = maxId + uint64(step) 130 | u.step = step 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/util/uid_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/pkg/db" 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | // 测试是否有序 and 重启后是否有序 11 | func TestUid_Seq_NextId(t *testing.T) { 12 | // 初始化配置 13 | config.InitConfig("../../app.yaml") 14 | db.InitMySQL(config.GlobalConfig.MySQL.DNS) 15 | 16 | uid := newUid("TestUid_Seq_NextId") 17 | 18 | for i := 0; i < UidStep*2-4; i++ { 19 | id, err := uid.nextId() 20 | if err != nil { 21 | t.Fatalf("unexpected error: %v", err) 22 | } 23 | t.Log(id) 24 | } 25 | } 26 | 27 | func TestUid_NextId(t *testing.T) { 28 | // 初始化配置 29 | config.InitConfig("../../app.yaml") 30 | db.InitMySQL(config.GlobalConfig.MySQL.DNS) 31 | 32 | // Create a new UID 33 | uid := newUid("test") 34 | 35 | // Test with multiple goroutines 36 | var wg sync.WaitGroup 37 | n := 10 38 | for i := 0; i < n; i++ { 39 | wg.Add(1) 40 | go func() { 41 | defer wg.Done() 42 | _, err := uid.nextId() 43 | if err != nil { 44 | t.Errorf("unexpected error: %v", err) 45 | } 46 | }() 47 | } 48 | wg.Wait() 49 | } 50 | 51 | func TestUidGenerator_GetNextIds(t *testing.T) { 52 | // 初始化配置 53 | config.InitConfig("../../app.yaml") 54 | db.InitMySQL(config.GlobalConfig.MySQL.DNS) 55 | 56 | gen := NewGeneratorUid() 57 | businessIds := []string{"user", "order", "product"} 58 | 59 | // 测试获取一批 id 60 | ids, err := gen.GetNextIds(businessIds) 61 | if err != nil { 62 | t.Errorf("Unexpected error: %v", err) 63 | } 64 | 65 | if len(ids) != len(businessIds) { 66 | t.Errorf("Expected %d ids, but got %d", len(businessIds), len(ids)) 67 | } 68 | 69 | // 测试获取多批 id 70 | ids1, err := gen.GetNextIds(businessIds) 71 | if err != nil { 72 | t.Errorf("Unexpected error: %v", err) 73 | } 74 | 75 | ids2, err := gen.GetNextIds(businessIds) 76 | if err != nil { 77 | t.Errorf("Unexpected error: %v", err) 78 | } 79 | 80 | if len(ids1) != len(businessIds) || len(ids2) != len(businessIds) { 81 | t.Errorf("Expected %d ids, but got %d and %d", len(businessIds), len(ids1), len(ids2)) 82 | } 83 | 84 | var wg sync.WaitGroup 85 | n := 10 86 | for i := 0; i < n; i++ { 87 | wg.Add(1) 88 | go func() { 89 | defer wg.Done() 90 | _, err := gen.GetNextIds(businessIds) 91 | if err != nil { 92 | t.Errorf("unexpected error: %v", err) 93 | } 94 | }() 95 | } 96 | wg.Wait() 97 | ids1, err = gen.GetNextIds(businessIds) 98 | if err != nil { 99 | t.Errorf("Unexpected error: %v", err) 100 | } 101 | t.Log(ids1) 102 | } 103 | 104 | func BenchmarkUidGenerator_GetNextIds(b *testing.B) { 105 | // 初始化配置 106 | config.InitConfig("../../app.yaml") 107 | db.InitMySQL(config.GlobalConfig.MySQL.DNS) 108 | 109 | gen := NewGeneratorUid() 110 | // 构造测试数据 111 | businessIds := make([]string, 10) 112 | for i := 0; i < 10; i++ { 113 | businessIds[i] = Int64ToStr(int64(i)) 114 | } 115 | 116 | b.ResetTimer() 117 | for i := 0; i < b.N; i++ { 118 | _, err := gen.GetNextIds(businessIds) 119 | if err != nil { 120 | b.Errorf("Unexpected error: %v", err) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /profile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callmePicacho/GoChat/c58f9fc9f821359f256bbf0ea2f9fcbad14129ee/profile -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/pkg/middlewares" 6 | "GoChat/service" 7 | "fmt" 8 | "github.com/gin-gonic/gin" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | // HTTPRouter http 路由 14 | func HTTPRouter() { 15 | r := gin.Default() 16 | 17 | gin.SetMode(gin.ReleaseMode) 18 | 19 | // 用户注册 20 | r.POST("/register", service.Register) 21 | 22 | // 用户登录 23 | r.POST("/login", service.Login) 24 | 25 | auth := r.Group("", middlewares.AuthCheck()) 26 | { 27 | // 添加好友 28 | auth.POST("/friend/add", service.AddFriend) 29 | 30 | // 创建群聊 31 | auth.POST("/group/create", service.CreateGroup) 32 | 33 | // 获取群成员列表 34 | auth.GET("/group_user/list", service.GroupUserList) 35 | } 36 | 37 | httpAddr := fmt.Sprintf("%s:%s", config.GlobalConfig.App.IP, config.GlobalConfig.App.HTTPServerPort) 38 | if err := r.Run(httpAddr); err != nil && err != http.ErrServerClosed { 39 | log.Fatalf("listen: %s\n", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /router/ws_router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/service/ws" 6 | "context" 7 | "fmt" 8 | "github.com/gin-contrib/pprof" 9 | "github.com/gin-gonic/gin" 10 | "github.com/gorilla/websocket" 11 | "log" 12 | "net/http" 13 | "os" 14 | "os/signal" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | var upgrader = websocket.Upgrader{ 20 | ReadBufferSize: 1024, 21 | WriteBufferSize: 1024, 22 | CheckOrigin: func(r *http.Request) bool { 23 | return true 24 | }, 25 | } 26 | 27 | // WSRouter websocket 路由 28 | func WSRouter() { 29 | server := ws.GetServer() 30 | 31 | // 开启worker工作池 32 | server.StartWorkerPool() 33 | 34 | // 开启心跳超时检测 35 | checker := ws.NewHeartbeatChecker(time.Second*time.Duration(config.GlobalConfig.App.HeartbeatInterval), server) 36 | go checker.Start() 37 | 38 | r := gin.Default() 39 | 40 | gin.SetMode(gin.ReleaseMode) 41 | 42 | pprof.Register(r) 43 | var connID uint64 44 | 45 | r.GET("/ws", func(c *gin.Context) { 46 | // 升级协议 http -> websocket 47 | WsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 48 | if err != nil { 49 | fmt.Println("websocket conn err :", err) 50 | return 51 | } 52 | 53 | // 初始化连接 54 | conn := ws.NewConnection(server, WsConn, connID) 55 | connID++ 56 | 57 | // 开启读写线程 58 | go conn.Start() 59 | }) 60 | 61 | srv := &http.Server{ 62 | Addr: fmt.Sprintf("%s:%s", config.GlobalConfig.App.IP, config.GlobalConfig.App.WebsocketPort), 63 | Handler: r, 64 | } 65 | 66 | go func() { 67 | fmt.Println("websocket 启动:", srv.Addr) 68 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 69 | log.Fatalf("listen: %s\n", err) 70 | } 71 | }() 72 | 73 | quit := make(chan os.Signal, 1) 74 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 75 | <-quit 76 | 77 | // 关闭服务 78 | server.Stop() 79 | checker.Stop() 80 | 81 | // 5s 超时 82 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 83 | defer cancel() 84 | if err := srv.Shutdown(ctx); err != nil { 85 | log.Fatal("Server Shutdown: ", err) 86 | } 87 | 88 | log.Println("Server exiting") 89 | } 90 | -------------------------------------------------------------------------------- /service/friend.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/model" 5 | "GoChat/pkg/util" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | // AddFriend 添加好友 11 | func AddFriend(c *gin.Context) { 12 | // 参数验证 13 | friendIdStr := c.PostForm("friend_id") 14 | friendId := util.StrToUint64(friendIdStr) 15 | if friendId == 0 { 16 | c.JSON(http.StatusOK, gin.H{ 17 | "code": -1, 18 | "msg": "参数不正确", 19 | }) 20 | return 21 | } 22 | // 获取自己的信息 23 | uc := c.MustGet("user_claims").(*util.UserClaims) 24 | if uc.UserId == friendId { 25 | c.JSON(http.StatusOK, gin.H{ 26 | "code": -1, 27 | "msg": "不能添加自己为好友", 28 | }) 29 | return 30 | } 31 | // 查询用户是否存在 32 | ub, err := model.GetUserById(friendId) 33 | if err != nil { 34 | c.JSON(http.StatusOK, gin.H{ 35 | "code": -1, 36 | "msg": "好友不存在", 37 | }) 38 | return 39 | } 40 | // 查询是否已建立好友关系 41 | isFriend, err := model.IsFriend(uc.UserId, ub.ID) 42 | if err != nil { 43 | c.JSON(http.StatusOK, gin.H{ 44 | "code": -1, 45 | "msg": "系统错误:" + err.Error(), 46 | }) 47 | return 48 | } 49 | if isFriend { 50 | c.JSON(http.StatusOK, gin.H{ 51 | "code": -1, 52 | "msg": "请勿重复添加", 53 | }) 54 | return 55 | } 56 | 57 | // 建立好友关系 58 | friend := &model.Friend{ 59 | UserID: uc.UserId, 60 | FriendID: ub.ID, 61 | } 62 | err = model.CreateFriend(friend) 63 | if err != nil { 64 | c.JSON(http.StatusOK, gin.H{ 65 | "code": -1, 66 | "msg": "系统错误:" + err.Error(), 67 | }) 68 | return 69 | } 70 | c.JSON(http.StatusOK, gin.H{ 71 | "code": 200, 72 | "msg": "好友添加成功", 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /service/group.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/lib/cache" 5 | "GoChat/model" 6 | "GoChat/pkg/util" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | // CreateGroup 创建群聊 12 | func CreateGroup(c *gin.Context) { 13 | // 参数校验 14 | name := c.PostForm("name") 15 | idsStr := c.PostFormArray("ids") // 群成员 id,不包括群创建者 16 | if name == "" || len(idsStr) == 0 { 17 | c.JSON(http.StatusOK, gin.H{ 18 | "code": -1, 19 | "msg": "参数不正确", 20 | }) 21 | return 22 | } 23 | ids := make([]uint64, 0, len(idsStr)+1) 24 | for i := range idsStr { 25 | ids = append(ids, util.StrToUint64(idsStr[i])) 26 | } 27 | // 获取用户信息 28 | uc := c.MustGet("user_claims").(*util.UserClaims) 29 | ids = append(ids, uc.UserId) 30 | 31 | // 获取 ids 用户信息 32 | ids, err := model.GetUserIdByIds(ids) 33 | if err != nil { 34 | c.JSON(http.StatusOK, gin.H{ 35 | "code": -1, 36 | "msg": "系统错误:" + err.Error(), 37 | }) 38 | return 39 | } 40 | 41 | // 创建群组 42 | group := &model.Group{ 43 | Name: name, 44 | OwnerID: uc.UserId, 45 | } 46 | err = model.CreateGroup(group, ids) 47 | if err != nil { 48 | c.JSON(http.StatusOK, gin.H{ 49 | "code": -1, 50 | "msg": "系统错误:" + err.Error(), 51 | }) 52 | return 53 | } 54 | // 将群成员信息更新到 Redis 55 | err = cache.SetGroupUser(group.ID, ids) 56 | if err != nil { 57 | c.JSON(http.StatusOK, gin.H{ 58 | "code": -1, 59 | "msg": "系统错误:" + err.Error(), 60 | }) 61 | return 62 | } 63 | 64 | c.JSON(http.StatusOK, gin.H{ 65 | "code": 200, 66 | "msg": "群组创建成功", 67 | "data": gin.H{ 68 | "id": util.Uint64ToStr(group.ID), 69 | }, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /service/group_user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/lib/cache" 5 | "GoChat/model" 6 | "GoChat/pkg/util" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | // GroupUserList 获取群成员列表 12 | func GroupUserList(c *gin.Context) { 13 | // 参数校验 14 | groupIdStr := c.Query("group_id") 15 | groupId := util.StrToUint64(groupIdStr) 16 | if groupId == 0 { 17 | c.JSON(http.StatusOK, gin.H{ 18 | "code": -1, 19 | "msg": "参数不正确", 20 | }) 21 | return 22 | } 23 | // 获取用户信息 24 | uc := c.MustGet("user_claims").(*util.UserClaims) 25 | 26 | // 验证用户是否属于该群 27 | isBelong, err := model.IsBelongToGroup(uc.UserId, groupId) 28 | if err != nil { 29 | c.JSON(http.StatusOK, gin.H{ 30 | "code": -1, 31 | "msg": "系统错误:" + err.Error(), 32 | }) 33 | return 34 | } 35 | if !isBelong { 36 | c.JSON(http.StatusOK, gin.H{ 37 | "code": -1, 38 | "msg": "用户不属于该群", 39 | }) 40 | return 41 | } 42 | 43 | // 获取群成员id列表 44 | ids, err := GetGroupUser(groupId) 45 | if err != nil { 46 | c.JSON(http.StatusOK, gin.H{ 47 | "code": -1, 48 | "msg": "系统错误:" + err.Error(), 49 | }) 50 | return 51 | } 52 | var idsStr []string 53 | for _, id := range ids { 54 | idsStr = append(idsStr, util.Uint64ToStr(id)) 55 | } 56 | c.JSON(http.StatusOK, gin.H{ 57 | "code": 200, 58 | "msg": "请求成功", 59 | "data": gin.H{ 60 | "ids": idsStr, 61 | }, 62 | }) 63 | } 64 | 65 | // GetGroupUser 获取群成员 66 | // 从缓存中获取,如果缓存中没有,获取后加入缓存 67 | func GetGroupUser(groupId uint64) ([]uint64, error) { 68 | userIds, err := cache.GetGroupUser(groupId) 69 | if err != nil { 70 | return nil, err 71 | } 72 | if len(userIds) != 0 { 73 | return userIds, nil 74 | } 75 | 76 | userIds, err = model.GetGroupUserIdsByGroupId(groupId) 77 | if err != nil { 78 | return nil, err 79 | } 80 | err = cache.SetGroupUser(groupId, userIds) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return userIds, nil 86 | } 87 | -------------------------------------------------------------------------------- /service/rpc_server/conn.go: -------------------------------------------------------------------------------- 1 | package rpc_server 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/pkg/protocol/pb" 6 | "GoChat/service/ws" 7 | "context" 8 | "fmt" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | "log" 12 | "net" 13 | ) 14 | 15 | type ConnectServer struct { 16 | pb.UnsafeConnectServer // 禁止向前兼容 17 | } 18 | 19 | func (*ConnectServer) DeliverMessage(ctx context.Context, req *pb.DeliverMessageReq) (*emptypb.Empty, error) { 20 | resp := &emptypb.Empty{} 21 | 22 | // 获取本地连接 23 | conn := ws.GetServer().GetConn(req.ReceiverId) 24 | if conn == nil || conn.GetUserId() != req.ReceiverId { 25 | fmt.Println("[DeliverMessage] 连接不存在 user_id:", req.ReceiverId) 26 | return resp, nil 27 | } 28 | 29 | // 消息发送 30 | conn.SendMsg(req.ReceiverId, req.Data) 31 | 32 | return resp, nil 33 | } 34 | 35 | func (*ConnectServer) DeliverMessageAll(ctx context.Context, req *pb.DeliverMessageAllReq) (*emptypb.Empty, error) { 36 | resp := &emptypb.Empty{} 37 | 38 | // 进行本地推送 39 | ws.GetServer().SendMessageAll(req.GetReceiverId_2Data()) 40 | 41 | return resp, nil 42 | } 43 | 44 | func InitRPCServer() { 45 | rpcPort := config.GlobalConfig.App.RPCPort 46 | 47 | server := grpc.NewServer() 48 | pb.RegisterConnectServer(server, &ConnectServer{}) 49 | 50 | listen, err := net.Listen("tcp", fmt.Sprintf(":%s", rpcPort)) 51 | if err != nil { 52 | log.Fatalf("failed to listen: %v", err) 53 | } 54 | fmt.Println("rpc server 启动 ", rpcPort) 55 | 56 | if err := server.Serve(listen); err != nil { 57 | log.Fatalf("failed to rpc serve: %v", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /service/seq.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/lib/cache" 5 | ) 6 | 7 | func GetUserNextSeq(userId uint64) (uint64, error) { 8 | return cache.GetNextSeqId(cache.SeqObjectTypeUser, userId) 9 | } 10 | 11 | func GetUserNextSeqBatch(userIds []uint64) ([]uint64, error) { 12 | return cache.GetNextSeqIds(cache.SeqObjectTypeUser, userIds) 13 | } 14 | -------------------------------------------------------------------------------- /service/uid.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/pkg/util" 5 | ) 6 | 7 | func GetUserNextId(userId uint64) (uint64, error) { 8 | return util.UidGen.GetNextId(util.Uint64ToStr(userId)) 9 | } 10 | 11 | // GetUserNextIdBatch 批量获取 seq 12 | func GetUserNextIdBatch(userIds []uint64) ([]uint64, error) { 13 | businessIds := make([]string, len(userIds)) 14 | for i, userId := range userIds { 15 | businessIds[i] = util.Uint64ToStr(userId) 16 | } 17 | return util.UidGen.GetNextIds(businessIds) 18 | } 19 | -------------------------------------------------------------------------------- /service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "GoChat/model" 5 | "GoChat/pkg/util" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | // Register 注册 11 | func Register(c *gin.Context) { 12 | // 获取参数并验证 13 | phoneNumber := c.PostForm("phone_number") 14 | nickname := c.PostForm("nickname") 15 | password := c.PostForm("password") 16 | if phoneNumber == "" || password == "" { 17 | c.JSON(http.StatusOK, gin.H{ 18 | "code": -1, 19 | "msg": "参数不正确", 20 | }) 21 | return 22 | } 23 | // 查询手机号是否已存在 24 | cnt, err := model.GetUserCountByPhone(nickname) 25 | if err != nil { 26 | c.JSON(http.StatusOK, gin.H{ 27 | "code": -1, 28 | "msg": "系统错误:" + err.Error(), 29 | }) 30 | return 31 | } 32 | if cnt > 0 { 33 | c.JSON(http.StatusOK, gin.H{ 34 | "code": -1, 35 | "msg": "账号已被注册", 36 | }) 37 | return 38 | } 39 | // 插入用户信息 40 | ub := &model.User{ 41 | PhoneNumber: phoneNumber, 42 | Nickname: nickname, 43 | Password: util.GetMD5(password), 44 | } 45 | err = model.CreateUser(ub) 46 | if err != nil { 47 | c.JSON(http.StatusOK, gin.H{ 48 | "code": -1, 49 | "msg": "系统错误" + err.Error(), 50 | }) 51 | return 52 | } 53 | 54 | // 生成 token 55 | token, err := util.GenerateToken(ub.ID) 56 | if err != nil { 57 | c.JSON(http.StatusOK, gin.H{ 58 | "code": -1, 59 | "msg": "系统错误:" + err.Error(), 60 | }) 61 | return 62 | } 63 | 64 | // 发放 token 65 | c.JSON(http.StatusOK, gin.H{ 66 | "code": 200, 67 | "msg": "登录成功", 68 | "data": gin.H{ 69 | "token": token, 70 | "id": util.Uint64ToStr(ub.ID), 71 | }, 72 | }) 73 | } 74 | 75 | // Login 登录 76 | func Login(c *gin.Context) { 77 | // 验证参数 78 | phoneNumber := c.PostForm("phone_number") 79 | password := c.PostForm("password") 80 | if phoneNumber == "" || password == "" { 81 | c.JSON(http.StatusOK, gin.H{ 82 | "code": -1, 83 | "msg": "参数不正确", 84 | }) 85 | return 86 | } 87 | 88 | // 验证账号名和密码是否正确 89 | ub, err := model.GetUserByPhoneAndPassword(phoneNumber, util.GetMD5(password)) 90 | if err != nil { 91 | c.JSON(http.StatusOK, gin.H{ 92 | "code": -1, 93 | "msg": "手机号或密码错误", 94 | }) 95 | return 96 | } 97 | // 生成 token 98 | token, err := util.GenerateToken(ub.ID) 99 | if err != nil { 100 | c.JSON(http.StatusOK, gin.H{ 101 | "code": -1, 102 | "msg": "系统错误:" + err.Error(), 103 | }) 104 | return 105 | } 106 | 107 | // 发放 token 108 | c.JSON(http.StatusOK, gin.H{ 109 | "code": 200, 110 | "msg": "登录成功", 111 | "data": gin.H{ 112 | "token": token, 113 | "user_id": util.Uint64ToStr(ub.ID), 114 | }, 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /service/ws/conn.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "GoChat/config" 5 | "GoChat/lib/cache" 6 | "GoChat/pkg/protocol/pb" 7 | "fmt" 8 | "github.com/gorilla/websocket" 9 | "google.golang.org/protobuf/proto" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Conn 连接实例 15 | // 1. 启动读写线程 16 | // 2. 读线程读到数据后,根据数据类型获取处理函数,交给 worker 队列调度执行 17 | type Conn struct { 18 | ConnId uint64 // 连接编号,通过对编号取余,能够让 Conn 始终进入同一个 worker,保持有序性 19 | server *Server // 当前连接属于哪个 server 20 | UserId uint64 // 连接所属用户id 21 | UserIdMutex sync.RWMutex // 保护 userId 的锁 22 | Socket *websocket.Conn // 用户连接 23 | sendCh chan []byte // 用户要发送的数据 24 | isClose bool // 连接状态 25 | isCloseMutex sync.RWMutex // 保护 isClose 的锁 26 | exitCh chan struct{} // 通知 writer 退出 27 | maxClientId uint64 // 该连接收到的最大 clientId,确保消息的可靠性 28 | maxClientIdMutex sync.Mutex // 保护 maxClientId 的锁 29 | 30 | lastHeartBeatTime time.Time // 最后活跃时间 31 | heartMutex sync.Mutex // 保护最后活跃时间的锁 32 | } 33 | 34 | func NewConnection(server *Server, wsConn *websocket.Conn, ConnId uint64) *Conn { 35 | return &Conn{ 36 | ConnId: ConnId, 37 | server: server, 38 | UserId: 0, // 此时用户未登录, userID 为 0 39 | Socket: wsConn, 40 | sendCh: make(chan []byte, 10), 41 | isClose: false, 42 | exitCh: make(chan struct{}, 1), 43 | lastHeartBeatTime: time.Now(), // 刚连接时初始化,避免正好遇到清理执行,如果连接没有后续操作,将会在下次被心跳检测踢出 44 | } 45 | } 46 | 47 | func (c *Conn) Start() { 48 | // 开启从客户端读取数据流程的 goroutine 49 | go c.StartReader() 50 | 51 | // 开启用于写回客户端数据流程的 goroutine 52 | //go c.StartWriter() 53 | go c.StartWriterWithBuffer() 54 | } 55 | 56 | // StartReader 用于从客户端中读取数据 57 | func (c *Conn) StartReader() { 58 | fmt.Println("[Reader Goroutine is running]") 59 | defer fmt.Println(c.RemoteAddr(), "[conn Reader exit!]") 60 | defer c.Stop() 61 | 62 | for { 63 | // 阻塞读 64 | _, data, err := c.Socket.ReadMessage() 65 | if err != nil { 66 | fmt.Println("read msg data error ", err) 67 | return 68 | } 69 | 70 | // 消息处理 71 | c.HandlerMessage(data) 72 | } 73 | } 74 | 75 | // HandlerMessage 消息处理 76 | func (c *Conn) HandlerMessage(bytes []byte) { 77 | // TODO 所有错误都需要写回给客户端 78 | // 消息解析 proto string -> struct 79 | input := new(pb.Input) 80 | err := proto.Unmarshal(bytes, input) 81 | if err != nil { 82 | fmt.Println("unmarshal error", err) 83 | return 84 | } 85 | //fmt.Println("收到消息:", input) 86 | 87 | // 对未登录用户进行拦截 88 | if input.Type != pb.CmdType_CT_Login && c.GetUserId() == 0 { 89 | return 90 | } 91 | 92 | req := &Req{ 93 | conn: c, 94 | data: input.Data, 95 | f: nil, 96 | } 97 | 98 | switch input.Type { 99 | case pb.CmdType_CT_Login: // 登录 100 | req.f = req.Login 101 | case pb.CmdType_CT_Heartbeat: // 心跳 102 | req.f = req.Heartbeat 103 | case pb.CmdType_CT_Message: // 上行消息 104 | req.f = req.MessageHandler 105 | case pb.CmdType_CT_ACK: // ACK TODO 106 | 107 | case pb.CmdType_CT_Sync: // 离线消息同步 108 | req.f = req.Sync 109 | default: 110 | fmt.Println("未知消息类型") 111 | } 112 | 113 | if req.f == nil { 114 | return 115 | } 116 | 117 | // 更新心跳时间 118 | c.KeepLive() 119 | 120 | // 送入worker队列等待调度执行 121 | c.server.SendMsgToTaskQueue(req) 122 | } 123 | 124 | // SendMsg 根据 userId 给相应 socket 发送消息 125 | func (c *Conn) SendMsg(userId uint64, bytes []byte) { 126 | c.isCloseMutex.RLock() 127 | defer c.isCloseMutex.RUnlock() 128 | 129 | // 已关闭 130 | if c.isClose { 131 | fmt.Println("connection closed when send msg") 132 | return 133 | } 134 | 135 | // 根据 userId 找到对应 socket 136 | conn := c.server.GetConn(userId) 137 | if conn == nil { 138 | return 139 | } 140 | 141 | // 发送 142 | conn.sendCh <- bytes 143 | 144 | return 145 | } 146 | 147 | // StartWriter 向客户端写数据 148 | func (c *Conn) StartWriter() { 149 | fmt.Println("[Writer Goroutine is running]") 150 | defer fmt.Println(c.RemoteAddr(), "[conn Writer exit!]") 151 | 152 | var err error 153 | for { 154 | select { 155 | case data := <-c.sendCh: 156 | if err = c.Socket.WriteMessage(websocket.BinaryMessage, data); err != nil { 157 | fmt.Println("Send Data error:, ", err, " Conn Writer exit") 158 | return 159 | } 160 | // 更新心跳时间 161 | c.KeepLive() 162 | case <-c.exitCh: 163 | return 164 | } 165 | } 166 | } 167 | 168 | // StartWriterWithBuffer 向客户端写数据 169 | // 由延迟优先调整为吞吐优先,使得消息的整体吞吐提升,但是单条消息的延迟会有所上升 170 | func (c *Conn) StartWriterWithBuffer() { 171 | fmt.Println("[Writer Goroutine is running]") 172 | defer fmt.Println(c.RemoteAddr(), "[conn Writer exit!]") 173 | 174 | // 每 100ms 或者当 buffer 中存够 50 条数据时,进行发送 175 | tickerInterval := 100 176 | ticker := time.NewTicker(time.Millisecond * time.Duration(tickerInterval)) 177 | bufferLimit := 50 178 | buffer := &pb.OutputBatch{Outputs: make([][]byte, 0, bufferLimit)} 179 | 180 | send := func() { 181 | if len(buffer.Outputs) == 0 { 182 | return 183 | } 184 | //fmt.Println("buffer 长度:", len(buffer.Outputs)) 185 | sendData, err := proto.Marshal(buffer) 186 | if err != nil { 187 | fmt.Println("send data proto.Marshal err:", err) 188 | return 189 | } 190 | if err = c.Socket.WriteMessage(websocket.BinaryMessage, sendData); err != nil { 191 | fmt.Println("Send Data error:, ", err, " Conn Writer exit") 192 | return 193 | } 194 | buffer.Outputs = make([][]byte, 0, bufferLimit) 195 | // 更新心跳时间 196 | c.KeepLive() 197 | } 198 | 199 | for { 200 | select { 201 | case buff := <-c.sendCh: 202 | buffer.Outputs = append(buffer.Outputs, buff) 203 | if len(buffer.Outputs) == bufferLimit { 204 | send() 205 | } 206 | case <-ticker.C: 207 | send() 208 | case <-c.exitCh: 209 | return 210 | } 211 | } 212 | } 213 | 214 | func (c *Conn) Stop() { 215 | c.isCloseMutex.Lock() 216 | defer c.isCloseMutex.Unlock() 217 | 218 | if c.isClose { 219 | return 220 | } 221 | 222 | // 关闭 socket 连接 223 | _ = c.Socket.Close() 224 | // 关闭 writer 225 | c.exitCh <- struct{}{} 226 | 227 | if c.GetUserId() != 0 { 228 | // 将连接从connMap中移除 229 | c.server.RemoveConn(c.GetUserId()) 230 | // 用户下线 231 | _ = cache.DelUserOnline(c.GetUserId()) 232 | } 233 | 234 | c.isClose = true 235 | 236 | // 关闭管道 237 | close(c.exitCh) 238 | close(c.sendCh) 239 | 240 | fmt.Println("Conn Stop() ... UserId = ", c.GetUserId()) 241 | } 242 | 243 | // KeepLive 更新心跳 244 | func (c *Conn) KeepLive() { 245 | now := time.Now() 246 | c.heartMutex.Lock() 247 | defer c.heartMutex.Unlock() 248 | 249 | c.lastHeartBeatTime = now 250 | } 251 | 252 | // IsAlive 是否存活 253 | func (c *Conn) IsAlive() bool { 254 | now := time.Now() 255 | 256 | c.heartMutex.Lock() 257 | c.isCloseMutex.RLock() 258 | defer c.isCloseMutex.RUnlock() 259 | defer c.heartMutex.Unlock() 260 | 261 | if c.isClose || now.Sub(c.lastHeartBeatTime) > time.Duration(config.GlobalConfig.App.HeartbeatTimeout)*time.Second { 262 | return false 263 | } 264 | return true 265 | } 266 | 267 | // GetUserId 获取 userId 268 | func (c *Conn) GetUserId() uint64 { 269 | c.UserIdMutex.RLock() 270 | defer c.UserIdMutex.RUnlock() 271 | 272 | return c.UserId 273 | } 274 | 275 | // SetUserId 设置 UserId 276 | func (c *Conn) SetUserId(userId uint64) { 277 | c.UserIdMutex.Lock() 278 | defer c.UserIdMutex.Unlock() 279 | 280 | c.UserId = userId 281 | } 282 | 283 | func (c *Conn) CompareAndIncrClientID(newMaxClientId uint64) bool { 284 | c.maxClientIdMutex.Lock() 285 | defer c.maxClientIdMutex.Unlock() 286 | 287 | //fmt.Println("收到的 newMaxClientId 是:", newMaxClientId, "此时 c.maxClientId 是:", c.maxClientId) 288 | if c.maxClientId+1 == newMaxClientId { 289 | c.maxClientId++ 290 | return true 291 | } 292 | return false 293 | } 294 | 295 | // RemoteAddr 获取远程客户端地址 296 | func (c *Conn) RemoteAddr() string { 297 | return c.Socket.RemoteAddr().String() 298 | } 299 | -------------------------------------------------------------------------------- /service/ws/heartbeat.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // HeartbeatChecker 心跳检测 9 | type HeartbeatChecker struct { 10 | interval time.Duration // 心跳检测时间间隔 11 | quit chan struct{} // 退出信号 12 | 13 | server *Server // 所属服务端 14 | } 15 | 16 | func NewHeartbeatChecker(interval time.Duration, s *Server) *HeartbeatChecker { 17 | return &HeartbeatChecker{ 18 | interval: interval, 19 | quit: make(chan struct{}, 1), 20 | server: s, 21 | } 22 | } 23 | 24 | // Start 启动心跳检测 25 | func (h *HeartbeatChecker) Start() { 26 | fmt.Println("HeartbeatChecker Start ... ") 27 | 28 | ticker := time.NewTicker(h.interval) 29 | for { 30 | select { 31 | case <-ticker.C: 32 | h.check() 33 | case <-h.quit: 34 | ticker.Stop() 35 | return 36 | } 37 | } 38 | } 39 | 40 | // Stop 停止心跳检测 41 | func (h *HeartbeatChecker) Stop() { 42 | h.quit <- struct{}{} 43 | } 44 | 45 | // check 超时检测 46 | func (h *HeartbeatChecker) check() { 47 | fmt.Println("heart check start...", time.Now().Format("2006-01-02 15:04:05")) 48 | // 已验证的连接 49 | conns := h.server.GetConnAll() 50 | for _, conn := range conns { 51 | if !conn.IsAlive() { 52 | conn.Stop() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /service/ws/message.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "GoChat/common" 5 | "GoChat/config" 6 | "GoChat/lib/cache" 7 | "GoChat/lib/mq" 8 | "GoChat/model" 9 | "GoChat/pkg/etcd" 10 | "GoChat/pkg/protocol/pb" 11 | "GoChat/pkg/rpc" 12 | "GoChat/service" 13 | "context" 14 | "fmt" 15 | "google.golang.org/protobuf/proto" 16 | "time" 17 | ) 18 | 19 | // GetOutputMsg 组装出下行消息 20 | func GetOutputMsg(cmdType pb.CmdType, code int32, message proto.Message) ([]byte, error) { 21 | output := &pb.Output{ 22 | Type: cmdType, 23 | Code: code, 24 | CodeMsg: common.GetErrorMessage(uint32(code), ""), 25 | Data: nil, 26 | } 27 | if message != nil { 28 | msgBytes, err := proto.Marshal(message) 29 | if err != nil { 30 | fmt.Println("[GetOutputMsg] message marshal err:", err) 31 | return nil, err 32 | } 33 | output.Data = msgBytes 34 | } 35 | 36 | bytes, err := proto.Marshal(output) 37 | if err != nil { 38 | fmt.Println("[GetOutputMsg] output marshal err:", err) 39 | return nil, err 40 | } 41 | return bytes, nil 42 | } 43 | 44 | // SendToUser 发送消息到好友 45 | func SendToUser(msg *pb.Message, userId uint64) (uint64, error) { 46 | // 获取接受者 seqId 47 | seq, err := service.GetUserNextSeq(userId) 48 | if err != nil { 49 | fmt.Println("[消息处理] 获取 seq 失败,err:", err) 50 | return 0, err 51 | } 52 | msg.Seq = seq 53 | 54 | // 发给MQ 55 | err = mq.MessageMQ.Publish(model.MessageToProtoMarshal(&model.Message{ 56 | UserID: userId, 57 | SenderID: msg.SenderId, 58 | SessionType: int8(msg.SessionType), 59 | ReceiverId: msg.ReceiverId, 60 | MessageType: int8(msg.MessageType), 61 | Content: msg.Content, 62 | Seq: seq, 63 | SendTime: time.UnixMilli(msg.SendTime), 64 | })) 65 | if err != nil { 66 | fmt.Println("[消息处理] mq.MessageMQ.Publish(messageBytes) 失败,err:", err) 67 | return 0, err 68 | } 69 | 70 | // 如果发给自己的,只落库不进行发送 71 | if userId == msg.SenderId { 72 | return seq, nil 73 | } 74 | 75 | // 组装消息 76 | bytes, err := GetOutputMsg(pb.CmdType_CT_Message, int32(common.OK), &pb.PushMsg{Msg: msg}) 77 | if err != nil { 78 | fmt.Println("[消息处理] GetOutputMsg Marshal error,err:", err) 79 | return 0, err 80 | } 81 | 82 | // 进行推送 83 | return 0, Send(userId, bytes) 84 | } 85 | 86 | // Send 消息转发 87 | // 是否在线 ---否---> 不进行推送 88 | // | 89 | // 是 90 | // ↓ 91 | // 是否在本地 --否--> RPC 调用 92 | // | 93 | // 是 94 | // ↓ 95 | // 消息发送 96 | 97 | func Send(receiverId uint64, bytes []byte) error { 98 | // 查询是否在线 99 | rpcAddr, err := cache.GetUserOnline(receiverId) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | // 不在线 105 | if rpcAddr == "" { 106 | fmt.Println("[消息处理],用户不在线,receiverId:", receiverId) 107 | return nil 108 | } 109 | 110 | fmt.Println("[消息处理] 用户在线,rpcAddr:", rpcAddr) 111 | 112 | // 查询是否在本地 113 | conn := ConnManager.GetConn(receiverId) 114 | if conn != nil { 115 | // 发送本地消息 116 | conn.SendMsg(receiverId, bytes) 117 | fmt.Println("[消息处理], 发送本地消息给用户, ", receiverId) 118 | return nil 119 | } 120 | 121 | // rpc 调用 122 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 123 | defer cancel() 124 | 125 | _, err = rpc.GetServerClient(rpcAddr).DeliverMessage(ctx, &pb.DeliverMessageReq{ 126 | ReceiverId: receiverId, 127 | Data: bytes, 128 | }) 129 | 130 | if err != nil { 131 | fmt.Println("[消息处理] DeliverMessage err, err:", err) 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // SendToGroup 发送消息到群 139 | func SendToGroup(msg *pb.Message) error { 140 | // 获取群成员信息 141 | userIds, err := service.GetGroupUser(msg.ReceiverId) 142 | if err != nil { 143 | fmt.Println("[群聊消息处理] 查询失败,err:", err, msg) 144 | return err 145 | } 146 | 147 | // userId set 148 | m := make(map[uint64]struct{}, len(userIds)) 149 | for _, userId := range userIds { 150 | m[userId] = struct{}{} 151 | } 152 | 153 | // 检查当前用户是否属于该群 154 | if _, ok := m[msg.SenderId]; !ok { 155 | fmt.Println("[群聊消息处理] 用户不属于该群组,msg:", msg) 156 | return nil 157 | } 158 | 159 | // 自己不再进行推送 160 | delete(m, msg.SenderId) 161 | 162 | sendUserIds := make([]uint64, 0, len(m)) 163 | for userId := range m { 164 | sendUserIds = append(sendUserIds, userId) 165 | } 166 | // 批量获取 seqId 167 | seqs, err := service.GetUserNextSeqBatch(sendUserIds) 168 | if err != nil { 169 | fmt.Println("[批量获取 seq] 失败,err:", err) 170 | return err 171 | } 172 | 173 | // k:userid v:该userId的seq 174 | sendUserSet := make(map[uint64]uint64, len(seqs)) 175 | for i, userId := range sendUserIds { 176 | sendUserSet[userId] = seqs[i] 177 | } 178 | 179 | // 创建 Message 对象 180 | messages := make([]*model.Message, 0, len(m)) 181 | for userId, seq := range sendUserSet { 182 | messages = append(messages, &model.Message{ 183 | UserID: userId, 184 | SenderID: msg.SenderId, 185 | SessionType: int8(msg.SessionType), 186 | ReceiverId: msg.ReceiverId, 187 | MessageType: int8(msg.MessageType), 188 | Content: msg.Content, 189 | Seq: seq, 190 | SendTime: time.UnixMilli(msg.SendTime), 191 | }) 192 | } 193 | 194 | // 发给MQ 195 | err = mq.MessageMQ.Publish(model.MessageToProtoMarshal(messages...)) 196 | if err != nil { 197 | fmt.Println("[消息处理] 群聊消息发送 MQ 失败,err:", err) 198 | return err 199 | } 200 | 201 | // 组装消息,进行推送 202 | userId2Msg := make(map[uint64][]byte, len(m)) 203 | for userId, seq := range sendUserSet { 204 | msg.Seq = seq 205 | bytes, err := GetOutputMsg(pb.CmdType_CT_Message, int32(common.OK), &pb.PushMsg{Msg: msg}) 206 | if err != nil { 207 | fmt.Println("[消息处理] GetOutputMsg Marshal error,err:", err) 208 | return err 209 | } 210 | userId2Msg[userId] = bytes 211 | } 212 | 213 | // 获取全部网关服务,进行消息推送 214 | services := etcd.DiscoverySer.GetServices() 215 | local := fmt.Sprintf("%s:%s", config.GlobalConfig.App.IP, config.GlobalConfig.App.RPCPort) 216 | for _, addr := range services { 217 | // 如果是本机,进行本地推送 218 | if local == addr { 219 | //fmt.Println("进行本地推送") 220 | GetServer().SendMessageAll(userId2Msg) 221 | } else { 222 | // fmt.Println("远端推送:", addr) 223 | // 如果不是本机,进行远程 RPC 调用 224 | _, err = rpc.GetServerClient(addr).DeliverMessageAll(context.Background(), &pb.DeliverMessageAllReq{ 225 | ReceiverId_2Data: userId2Msg, 226 | }) 227 | 228 | if err != nil { 229 | fmt.Println("[消息处理] DeliverMessageAll err, err:", err) 230 | return err 231 | } 232 | } 233 | } 234 | 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /service/ws/req.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "GoChat/common" 5 | "GoChat/config" 6 | "GoChat/lib/cache" 7 | "GoChat/model" 8 | "GoChat/pkg/protocol/pb" 9 | "GoChat/pkg/util" 10 | "fmt" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | // Handler 路由函数 15 | type Handler func() 16 | 17 | // Req 请求 18 | type Req struct { 19 | conn *Conn // 连接 20 | data []byte // 客户端发送的请求数据 21 | f Handler // 该请求需要执行的路由函数 22 | } 23 | 24 | func (r *Req) Login() { 25 | // 检查用户是否已登录 只能防止同一个连接多次调用 Login 26 | if r.conn.GetUserId() != 0 { 27 | fmt.Println("[用户登录] 用户已登录") 28 | return 29 | } 30 | 31 | // 消息解析 proto string -> struct 32 | loginMsg := new(pb.LoginMsg) 33 | err := proto.Unmarshal(r.data, loginMsg) 34 | if err != nil { 35 | fmt.Println("[用户登录] unmarshal error,err:", err) 36 | return 37 | } 38 | // 登录校验 39 | userClaims, err := util.AnalyseToken(string(loginMsg.Token)) 40 | if err != nil { 41 | fmt.Println("[用户登录] AnalyseToken err:", err) 42 | return 43 | } 44 | 45 | // 检查用户是否已经在其他连接登录 46 | onlineAddr, err := cache.GetUserOnline(userClaims.UserId) 47 | if onlineAddr != "" { 48 | // TODO 更友好的提示 49 | fmt.Println("[用户登录] 用户已经在其他连接登录") 50 | r.conn.Stop() 51 | return 52 | } 53 | 54 | // Redis 存储用户数据 k: userId, v: grpc地址,方便用户能直接通过这个地址进行 rpc 方法调用 55 | grpcServerAddr := fmt.Sprintf("%s:%s", config.GlobalConfig.App.IP, config.GlobalConfig.App.RPCPort) 56 | err = cache.SetUserOnline(userClaims.UserId, grpcServerAddr) 57 | if err != nil { 58 | fmt.Println("[用户登录] 系统错误") 59 | return 60 | } 61 | 62 | // 设置 user_id 63 | r.conn.SetUserId(userClaims.UserId) 64 | 65 | // 加入到 connMap 中 66 | r.conn.server.AddConn(userClaims.UserId, r.conn) 67 | 68 | // 回复ACK 69 | bytes, err := GetOutputMsg(pb.CmdType_CT_ACK, int32(common.OK), &pb.ACKMsg{Type: pb.ACKType_AT_Login}) 70 | if err != nil { 71 | fmt.Println("[用户登录] proto.Marshal err:", err) 72 | return 73 | } 74 | 75 | // 回复发送 Login 请求的客户端 76 | r.conn.SendMsg(userClaims.UserId, bytes) 77 | } 78 | 79 | func (r *Req) Heartbeat() { 80 | // TODO 更新当前用户状态,不做回复 81 | } 82 | 83 | // MessageHandler 消息处理,处理客户端发送给服务端的消息 84 | // A 发送消息给服务端,服务端收到消息处理后发给 B 85 | // 包括:单聊、群聊 86 | func (r *Req) MessageHandler() { 87 | // 消息解析 proto string -> struct 88 | msg := new(pb.UpMsg) 89 | err := proto.Unmarshal(r.data, msg) 90 | if err != nil { 91 | fmt.Println("[消息处理] unmarshal error,err:", err) 92 | return 93 | } 94 | 95 | // 实现消息可靠性 96 | if !r.conn.CompareAndIncrClientID(msg.ClientId) { 97 | fmt.Println("不是想要收到的 clientID,不进行处理, msg:", msg) 98 | return 99 | } 100 | 101 | if msg.Msg.SenderId != r.conn.GetUserId() { 102 | fmt.Println("[消息处理] 发送者有误") 103 | return 104 | } 105 | 106 | // 单聊不能发给自己 107 | if msg.Msg.SessionType == pb.SessionType_ST_Single && msg.Msg.ReceiverId == r.conn.GetUserId() { 108 | fmt.Println("[消息处理] 接收者有误") 109 | return 110 | } 111 | 112 | // 给自己发一份,消息落库但是不推送 113 | seq, err := SendToUser(msg.Msg, msg.Msg.SenderId) 114 | if err != nil { 115 | fmt.Println("[消息处理] send to 自己出错, err:", err) 116 | return 117 | } 118 | 119 | // 单聊、群聊 120 | switch msg.Msg.SessionType { 121 | case pb.SessionType_ST_Single: 122 | _, err = SendToUser(msg.Msg, msg.Msg.ReceiverId) 123 | case pb.SessionType_ST_Group: 124 | err = SendToGroup(msg.Msg) 125 | default: 126 | fmt.Println("[消息处理] 会话类型错误") 127 | return 128 | } 129 | if err != nil { 130 | fmt.Println("[消息处理] 系统错误") 131 | return 132 | } 133 | 134 | // 回复发送上行消息的客户端 ACK 135 | ackBytes, err := GetOutputMsg(pb.CmdType_CT_ACK, common.OK, &pb.ACKMsg{ 136 | Type: pb.ACKType_AT_Up, 137 | ClientId: msg.ClientId, // 回复客户端,当前已 ACK 的消息 138 | Seq: seq, // 回复客户端当前其 seq 139 | }) 140 | if err != nil { 141 | fmt.Println("[消息处理] proto.Marshal err:", err) 142 | return 143 | } 144 | // 回复发送 Message 请求的客户端 A 145 | r.conn.SendMsg(msg.Msg.SenderId, ackBytes) 146 | } 147 | 148 | // Sync 消息同步,拉取离线消息 149 | func (r *Req) Sync() { 150 | msg := new(pb.SyncInputMsg) 151 | err := proto.Unmarshal(r.data, msg) 152 | if err != nil { 153 | fmt.Println("[离线消息] unmarshal error,err:", err) 154 | return 155 | } 156 | 157 | // 根据 seq 查询,得到比 seq 大的用户消息 158 | messages, hasMore, err := model.ListByUserIdAndSeq(r.conn.GetUserId(), msg.Seq, model.MessageLimit) 159 | if err != nil { 160 | fmt.Println("[离线消息] model.ListByUserIdAndSeq error, err:", err) 161 | return 162 | } 163 | pbMessage := model.MessagesToPB(messages) 164 | 165 | ackBytes, err := GetOutputMsg(pb.CmdType_CT_Sync, int32(common.OK), &pb.SyncOutputMsg{ 166 | Messages: pbMessage, 167 | HasMore: hasMore, 168 | }) 169 | if err != nil { 170 | fmt.Println("[离线消息] proto.Marshal err:", err) 171 | return 172 | } 173 | // 回复 174 | r.conn.SendMsg(r.conn.GetUserId(), ackBytes) 175 | } 176 | -------------------------------------------------------------------------------- /service/ws/server.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "GoChat/config" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | ConnManager *Server 11 | once sync.Once 12 | ) 13 | 14 | // Server 连接管理 15 | // 1. 连接管理 16 | // 2. 工作队列 17 | type Server struct { 18 | connMap sync.Map // 登录的用户连接 k-用户userid v-连接 19 | taskQueue []chan *Req // 工作池 20 | } 21 | 22 | func GetServer() *Server { 23 | once.Do(func() { 24 | ConnManager = &Server{ 25 | taskQueue: make([]chan *Req, config.GlobalConfig.App.WorkerPoolSize), // 初始worker队列中,worker个数 26 | } 27 | }) 28 | return ConnManager 29 | } 30 | 31 | // Stop 关闭服务 32 | func (cm *Server) Stop() { 33 | fmt.Println("server stop ...") 34 | ch := make(chan struct{}, 1000) // 控制并发数 35 | var wg sync.WaitGroup 36 | connAll := cm.GetConnAll() 37 | for _, conn := range connAll { 38 | ch <- struct{}{} 39 | wg.Add(1) 40 | c := conn 41 | go func() { 42 | defer func() { 43 | wg.Done() 44 | <-ch 45 | }() 46 | c.Stop() 47 | }() 48 | } 49 | close(ch) 50 | wg.Wait() 51 | } 52 | 53 | // AddConn 添加连接 54 | func (cm *Server) AddConn(userId uint64, conn *Conn) { 55 | cm.connMap.Store(userId, conn) 56 | fmt.Printf("connection UserId=%d add to Server\n", userId) 57 | } 58 | 59 | // RemoveConn 删除连接 60 | func (cm *Server) RemoveConn(userId uint64) { 61 | cm.connMap.Delete(userId) 62 | fmt.Printf("connection UserId=%d remove from Server\n", userId) 63 | } 64 | 65 | // GetConn 根据userid获取相应的连接 66 | func (cm *Server) GetConn(userId uint64) *Conn { 67 | value, ok := cm.connMap.Load(userId) 68 | if ok { 69 | return value.(*Conn) 70 | } 71 | return nil 72 | } 73 | 74 | // GetConnAll 获取全部连接 75 | func (cm *Server) GetConnAll() []*Conn { 76 | conns := make([]*Conn, 0) 77 | cm.connMap.Range(func(key, value interface{}) bool { 78 | conn := value.(*Conn) 79 | conns = append(conns, conn) 80 | return true 81 | }) 82 | return conns 83 | } 84 | 85 | // SendMessageAll 进行本地推送 86 | func (cm *Server) SendMessageAll(userId2Msg map[uint64][]byte) { 87 | var wg sync.WaitGroup 88 | ch := make(chan struct{}, 5) // 限制并发数 89 | for userId, data := range userId2Msg { 90 | ch <- struct{}{} 91 | wg.Add(1) 92 | go func(userId uint64, data []byte) { 93 | defer func() { 94 | <-ch 95 | wg.Done() 96 | }() 97 | conn := ConnManager.GetConn(userId) 98 | if conn != nil { 99 | conn.SendMsg(userId, data) 100 | } 101 | }(userId, data) 102 | } 103 | close(ch) 104 | wg.Wait() 105 | } 106 | 107 | // StartWorkerPool 启动 worker 工作池 108 | func (cm *Server) StartWorkerPool() { 109 | // 初始化并启动 worker 工作池 110 | for i := 0; i < len(cm.taskQueue); i++ { 111 | // 初始化 112 | cm.taskQueue[i] = make(chan *Req, config.GlobalConfig.App.MaxWorkerTask) // 初始化worker队列中,每个worker的队列长度 113 | // 启动worker 114 | go cm.StartOneWorker(i, cm.taskQueue[i]) 115 | } 116 | } 117 | 118 | // StartOneWorker 启动 worker 的工作流程 119 | func (cm *Server) StartOneWorker(workerID int, taskQueue chan *Req) { 120 | fmt.Println("Worker ID = ", workerID, " is started.") 121 | for { 122 | select { 123 | case req := <-taskQueue: 124 | req.f() 125 | } 126 | } 127 | } 128 | 129 | // SendMsgToTaskQueue 将消息交给 taskQueue,由 worker 调度处理 130 | func (cm *Server) SendMsgToTaskQueue(req *Req) { 131 | if len(cm.taskQueue) > 0 { 132 | // 根据ConnID来分配当前的连接应该由哪个worker负责处理,保证同一个连接的消息处理串行 133 | // 轮询的平均分配法则 134 | 135 | //得到需要处理此条连接的workerID 136 | workerID := req.conn.ConnId % uint64(len(cm.taskQueue)) 137 | 138 | // 将消息发给对应的 taskQueue 139 | cm.taskQueue[workerID] <- req 140 | } else { 141 | // 可能导致消息乱序 142 | go req.f() 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /sql/create_table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `user`; 2 | CREATE TABLE `user` 3 | ( 4 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 5 | `phone_number` varchar(20) NOT NULL COMMENT '手机号', 6 | `nickname` varchar(20) NOT NULL COMMENT '昵称', 7 | `password` varchar(255) NOT NULL COMMENT '密码', 8 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 9 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 10 | PRIMARY KEY (`id`), 11 | UNIQUE KEY `uk_phone_number` (`phone_number`) 12 | ) ENGINE = InnoDB 13 | DEFAULT CHARSET = utf8mb4; 14 | 15 | DROP TABLE IF EXISTS `friend`; 16 | CREATE TABLE `friend` 17 | ( 18 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 19 | `user_id` bigint unsigned NOT NULL COMMENT '用户id', 20 | `friend_id` bigint unsigned NOT NULL COMMENT '好友id', 21 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 22 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 23 | PRIMARY KEY (`id`), 24 | UNIQUE KEY `uk_user_id_friend_id` (`user_id`, `friend_id`) 25 | ) ENGINE = InnoDB 26 | DEFAULT CHARSET = utf8mb4; 27 | 28 | DROP TABLE IF EXISTS `group`; 29 | CREATE TABLE `group` 30 | ( 31 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 32 | `name` varchar(50) NOT NULL COMMENT '群组名称', 33 | `owner_id` bigint unsigned NOT NULL COMMENT '群主id', 34 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 35 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 36 | PRIMARY KEY (`id`) 37 | ) ENGINE = InnoDB 38 | DEFAULT CHARSET = utf8mb4; 39 | 40 | DROP TABLE IF EXISTS `group_user`; 41 | CREATE TABLE `group_user` 42 | ( 43 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 44 | `group_id` bigint unsigned NOT NULL COMMENT '群组id', 45 | `user_id` bigint unsigned NOT NULL COMMENT '用户id', 46 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 47 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 48 | PRIMARY KEY (`id`), 49 | UNIQUE KEY `uk_group_id_user_id` (`group_id`, `user_id`), 50 | KEY `idx_user_id` (`user_id`) 51 | ) ENGINE = InnoDB 52 | DEFAULT CHARSET = utf8mb4; 53 | 54 | DROP TABLE IF EXISTS `message`; 55 | CREATE TABLE `message` 56 | ( 57 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 58 | `user_id` bigint unsigned NOT NULL COMMENT '用户id,指接受者用户id', 59 | `sender_id` bigint unsigned NOT NULL COMMENT '发送者用户id', 60 | `session_type` tinyint NOT NULL COMMENT '聊天类型,群聊/单聊', 61 | `receiver_id` bigint unsigned NOT NULL COMMENT '接收者id,群聊id/用户id', 62 | `message_type` tinyint NOT NULL COMMENT '消息类型,语言、文字、图片', 63 | `content` blob NOT NULL COMMENT '消息内容', 64 | `seq` bigint unsigned NOT NULL COMMENT '消息序列号', 65 | `send_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息发送时间', 66 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 67 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 68 | PRIMARY KEY (`id`), 69 | UNIQUE KEY `uk_user_id_seq` (`user_id`, `seq`) USING BTREE 70 | ) ENGINE = InnoDB 71 | DEFAULT CHARSET = utf8mb4; 72 | 73 | DROP TABLE IF EXISTS `uid`; 74 | CREATE TABLE `uid` 75 | ( 76 | `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 77 | `business_id` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务id', 78 | `max_id` bigint unsigned DEFAULT NULL COMMENT '最大id', 79 | `step` int unsigned DEFAULT NULL COMMENT '步长', 80 | `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 81 | `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 82 | PRIMARY KEY (`id`), 83 | UNIQUE KEY `uk_business_id` (`business_id`) 84 | ) ENGINE = InnoDB 85 | DEFAULT CHARSET = utf8mb4 86 | COLLATE = utf8mb4_bin COMMENT ='分布式自增主键'; -------------------------------------------------------------------------------- /test/router_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | const ( 14 | httpURL = "http://localhost:9090" 15 | contentType = "application/x-www-form-urlencoded" 16 | ) 17 | 18 | // 注册用户 19 | func TestRegister(t *testing.T) { 20 | // 创建一个 http.Client 21 | client := &http.Client{} 22 | 23 | // 准备要发送的表单数据 24 | data := url.Values{} 25 | data.Set("phone_number", "55555") 26 | data.Set("nickname", "test") 27 | data.Set("password", "123") 28 | 29 | // 创建一个 POST 请求 30 | req, err := http.NewRequest("POST", httpURL+"/register", strings.NewReader(data.Encode())) 31 | if err != nil { 32 | // 处理错误 33 | return 34 | } 35 | req.Header.Set("Content-Type", contentType) 36 | 37 | // 发送请求并获取响应 38 | resp, err := client.Do(req) 39 | if err != nil { 40 | // 处理错误 41 | return 42 | } 43 | defer resp.Body.Close() 44 | 45 | responseBody, err := ioutil.ReadAll(resp.Body) 46 | if err != nil { 47 | // 处理读取错误 48 | panic(err) 49 | } 50 | 51 | var respData struct { 52 | Code int `json:"code"` 53 | Msg string `json:"msg"` 54 | Data struct { 55 | Token string `json:"token"` 56 | Id string `json:"id"` 57 | } `json:"data"` 58 | } 59 | err = json.Unmarshal(responseBody, &respData) 60 | if err != nil { 61 | panic(err) 62 | } 63 | t.Log(respData) 64 | } 65 | 66 | // 登录 67 | func TestLogin(t *testing.T) { 68 | // 创建一个 http.Client 69 | client := &http.Client{} 70 | 71 | // 准备要发送的表单数据 72 | data := url.Values{} 73 | data.Set("phone_number", "123456789") 74 | data.Set("password", "123") 75 | 76 | // 创建一个 POST 请求 77 | req, err := http.NewRequest("POST", httpURL+"/login", strings.NewReader(data.Encode())) 78 | if err != nil { 79 | // 处理错误 80 | return 81 | } 82 | req.Header.Set("Content-Type", contentType) 83 | 84 | // 发送请求并获取响应 85 | resp, err := client.Do(req) 86 | if err != nil { 87 | // 处理错误 88 | return 89 | } 90 | defer resp.Body.Close() 91 | 92 | responseBody, err := ioutil.ReadAll(resp.Body) 93 | if err != nil { 94 | // 处理读取错误 95 | panic(err) 96 | } 97 | var respData struct { 98 | Code int `json:"code"` 99 | Msg string `json:"msg"` 100 | Data struct { 101 | Token string `json:"token"` 102 | UserId string `json:"user_id"` 103 | } `json:"data"` 104 | } 105 | err = json.Unmarshal(responseBody, &respData) 106 | if err != nil { 107 | panic(err) 108 | } 109 | t.Log(respData) 110 | } 111 | 112 | func TestAddFriend(t *testing.T) { 113 | // 创建一个 http.Client 114 | client := &http.Client{} 115 | 116 | // 准备要发送的表单数据 117 | data := url.Values{} 118 | data.Set("friend_id", "6") 119 | 120 | // 创建一个 POST 请求 121 | req, err := http.NewRequest("POST", httpURL+"/friend/add", strings.NewReader(data.Encode())) 122 | if err != nil { 123 | // 处理错误 124 | return 125 | } 126 | req.Header.Set("Content-Type", contentType) 127 | // 设置 token 128 | //req.Header.Set("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiZXhwIjoxNjgyNTcyNTI5fQ.uHn7XSVb2T4cBUC6gBE8-iQbnI_pqB0bWFPAkOtQmPk") 129 | req.Header.Set("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiZXhwIjoxNjgyNTczODcxfQ.Ksw5J8vfVkUPPmM-2EJeiMFr9THqKhvlRKIR_W4H3SE") 130 | 131 | // 发送请求并获取响应 132 | resp, err := client.Do(req) 133 | if err != nil { 134 | // 处理错误 135 | return 136 | } 137 | defer resp.Body.Close() 138 | 139 | responseBody, err := ioutil.ReadAll(resp.Body) 140 | if err != nil { 141 | // 处理读取错误 142 | panic(err) 143 | } 144 | fmt.Println(string(responseBody)) 145 | var respData struct { 146 | Code int `json:"code"` 147 | Msg string `json:"msg"` 148 | } 149 | err = json.Unmarshal(responseBody, &respData) 150 | if err != nil { 151 | panic(err) 152 | } 153 | t.Log(respData) 154 | } 155 | 156 | func TestCreateGroup(t *testing.T) { 157 | // 创建一个 http.Client 158 | client := &http.Client{} 159 | 160 | // 准备要发送的表单数据 161 | data := url.Values{} 162 | data.Set("name", "6") 163 | ids := []string{"1", "2", "3", "4", "5", "6", "7"} 164 | for _, id := range ids { 165 | data.Add("ids", id) 166 | } 167 | 168 | // 创建一个 POST 请求 169 | req, err := http.NewRequest("POST", httpURL+"/group/create", strings.NewReader(data.Encode())) 170 | if err != nil { 171 | // 处理错误 172 | return 173 | } 174 | req.Header.Set("Content-Type", contentType) 175 | // 设置 token 176 | req.Header.Set("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiZXhwIjoxNjgyNTczODcxfQ.Ksw5J8vfVkUPPmM-2EJeiMFr9THqKhvlRKIR_W4H3SE") 177 | 178 | // 发送请求并获取响应 179 | resp, err := client.Do(req) 180 | if err != nil { 181 | // 处理错误 182 | return 183 | } 184 | defer resp.Body.Close() 185 | 186 | responseBody, err := ioutil.ReadAll(resp.Body) 187 | if err != nil { 188 | // 处理读取错误 189 | panic(err) 190 | } 191 | fmt.Println(string(responseBody)) 192 | var respData struct { 193 | Code int `json:"code"` 194 | Msg string `json:"msg"` 195 | Data struct { 196 | Id string `json:"id"` 197 | } `json:"data"` 198 | } 199 | err = json.Unmarshal(responseBody, &respData) 200 | if err != nil { 201 | panic(err) 202 | } 203 | t.Log(respData) 204 | } 205 | 206 | func TestGroupUserList(t *testing.T) { 207 | // 创建一个 http.Client 208 | client := &http.Client{} 209 | 210 | // 准备要发送的表单数据 211 | data := url.Values{} 212 | data.Set("group_id", "1") 213 | 214 | // 创建一个 POST 请求 215 | req, err := http.NewRequest("GET", httpURL+"/group_user/list?"+data.Encode(), nil) 216 | if err != nil { 217 | // 处理错误 218 | return 219 | } 220 | // 设置 token 221 | req.Header.Set("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiZXhwIjoxNjgyNTczODcxfQ.Ksw5J8vfVkUPPmM-2EJeiMFr9THqKhvlRKIR_W4H3SE") 222 | 223 | // 发送请求并获取响应 224 | resp, err := client.Do(req) 225 | if err != nil { 226 | // 处理错误 227 | return 228 | } 229 | defer resp.Body.Close() 230 | 231 | responseBody, err := ioutil.ReadAll(resp.Body) 232 | if err != nil { 233 | // 处理读取错误 234 | panic(err) 235 | } 236 | fmt.Println(string(responseBody)) 237 | var respData struct { 238 | Code int `json:"code"` 239 | Msg string `json:"msg"` 240 | Data struct { 241 | Ids []string `json:"ids"` 242 | } `json:"data"` 243 | } 244 | err = json.Unmarshal(responseBody, &respData) 245 | if err != nil { 246 | panic(err) 247 | } 248 | t.Log(respData) 249 | } 250 | -------------------------------------------------------------------------------- /test/ws_benchmark/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "GoChat/pkg/protocol/pb" 5 | "GoChat/pkg/util" 6 | "context" 7 | "fmt" 8 | "github.com/gorilla/websocket" 9 | "google.golang.org/protobuf/proto" 10 | "net/http" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | const ( 17 | ResendCountMax = 3 // 超时重传最大次数 18 | ) 19 | 20 | type Client struct { 21 | conn *websocket.Conn 22 | token string 23 | userId uint64 24 | clientId uint64 25 | clientId2Cancel map[uint64]context.CancelFunc // clientId 到 context 的映射 26 | clientId2CancelMutex sync.Mutex 27 | seq uint64 // 本地消息最大同步序列号 28 | 29 | sendCh chan []byte // 写入 30 | } 31 | 32 | func NewClient(userId, token, host string) *Client { 33 | // 创建 client 34 | c := &Client{ 35 | clientId2Cancel: make(map[uint64]context.CancelFunc), 36 | token: token, 37 | userId: util.StrToUint64(userId), 38 | sendCh: make(chan []byte, 1024), 39 | } 40 | 41 | // 连接 websocket 42 | conn, _, err := websocket.DefaultDialer.Dial(host+"/ws", http.Header{}) 43 | if err != nil { 44 | panic(err) 45 | } 46 | c.conn = conn 47 | // 向 websocket 发送登录请求 48 | c.login() 49 | 50 | // 心跳 51 | go c.heartbeat() 52 | 53 | // 写 54 | go c.write() 55 | 56 | // 读 57 | go c.read() 58 | 59 | return c 60 | } 61 | 62 | func (c *Client) read() { 63 | for { 64 | _, bytes, err := c.conn.ReadMessage() 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | outputBatchMsg := new(pb.OutputBatch) 70 | err = proto.Unmarshal(bytes, outputBatchMsg) 71 | if err != nil { 72 | panic(err) 73 | } 74 | for _, output := range outputBatchMsg.Outputs { 75 | msg := new(pb.Output) 76 | err = proto.Unmarshal(output, msg) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | // 只收两种,Message 收取下行消息和 ACK,上行消息ACK回复 82 | switch msg.Type { 83 | case pb.CmdType_CT_Message: 84 | // 计算接收消息数量 85 | atomic.AddInt64(&receivedMessageCount, 1) 86 | msgTimer.updateEndTime() 87 | 88 | pushMsg := new(pb.PushMsg) 89 | err = proto.Unmarshal(msg.Data, pushMsg) 90 | if err != nil { 91 | panic(err) 92 | } 93 | // 更新 seq 94 | seq := pushMsg.Msg.Seq 95 | if c.seq < seq { 96 | c.seq = seq 97 | } 98 | case pb.CmdType_CT_ACK: // 收到 ACK 99 | ackMsg := new(pb.ACKMsg) 100 | err = proto.Unmarshal(msg.Data, ackMsg) 101 | if err != nil { 102 | panic(err) 103 | } 104 | 105 | switch ackMsg.Type { 106 | case pb.ACKType_AT_Up: // 收到上行消息的 ACK 107 | // 计算接收消息数量 108 | atomic.AddInt64(&receivedMessageCount, 1) 109 | msgTimer.updateEndTime() 110 | 111 | // 取消超时重传 112 | clientId := ackMsg.ClientId 113 | c.clientId2CancelMutex.Lock() 114 | if cancel, ok := c.clientId2Cancel[clientId]; ok { 115 | // 取消超时重传 116 | cancel() 117 | delete(c.clientId2Cancel, clientId) 118 | //fmt.Println("取消超时重传,clientId:", clientId) 119 | } 120 | c.clientId2CancelMutex.Unlock() 121 | // 更新客户端本地维护的 seq 122 | seq := ackMsg.Seq 123 | if c.seq < seq { 124 | c.seq = seq 125 | } 126 | } 127 | default: 128 | fmt.Println("未知消息类型") 129 | } 130 | } 131 | } 132 | } 133 | 134 | func (c *Client) write() { 135 | for { 136 | select { 137 | case bytes, ok := <-c.sendCh: 138 | if !ok { 139 | return 140 | } 141 | if err := c.conn.WriteMessage(websocket.BinaryMessage, bytes); err != nil { 142 | return 143 | } 144 | } 145 | } 146 | } 147 | 148 | func (c *Client) heartbeat() { 149 | ticker := time.NewTicker(time.Minute * 2) 150 | for range ticker.C { 151 | c.sendMsg(pb.CmdType_CT_Heartbeat, &pb.HeartbeatMsg{}) 152 | } 153 | } 154 | 155 | func (c *Client) login() { 156 | c.sendMsg(pb.CmdType_CT_Login, &pb.LoginMsg{ 157 | Token: []byte(c.token), 158 | }) 159 | } 160 | 161 | // send 发送消息,启动超时重试 162 | func (c *Client) send(chatId int64) { 163 | message := &pb.Message{ 164 | SessionType: pb.SessionType_ST_Group, // 群聊 165 | ReceiverId: uint64(chatId), // 发送到该群 166 | SenderId: c.userId, // 发送者 167 | MessageType: pb.MessageType_MT_Text, // 文本 168 | Content: []byte("文本聊天消息" + util.Uint64ToStr(c.userId)), // 消息 169 | SendTime: time.Now().UnixMilli(), // 发送时间 170 | } 171 | UpMsg := &pb.UpMsg{ 172 | Msg: message, 173 | ClientId: c.getClientId(), 174 | } 175 | // 发送消息 176 | c.sendMsg(pb.CmdType_CT_Message, UpMsg) 177 | 178 | // 启动超时重传 179 | ctx, cancel := context.WithCancel(context.Background()) 180 | 181 | go func(ctx context.Context) { 182 | maxRetry := ResendCountMax // 最大重试次数 183 | retryCount := 0 184 | retryInterval := time.Millisecond * 500 // 重试间隔 185 | ticker := time.NewTicker(retryInterval) 186 | defer ticker.Stop() 187 | for { 188 | select { 189 | case <-ctx.Done(): 190 | return 191 | case <-ticker.C: 192 | if retryCount >= maxRetry { 193 | return 194 | } 195 | c.sendMsg(pb.CmdType_CT_Message, UpMsg) 196 | retryCount++ 197 | } 198 | } 199 | }(ctx) 200 | 201 | c.clientId2CancelMutex.Lock() 202 | c.clientId2Cancel[UpMsg.ClientId] = cancel 203 | c.clientId2CancelMutex.Unlock() 204 | 205 | } 206 | 207 | func (c *Client) getClientId() uint64 { 208 | c.clientId++ 209 | return c.clientId 210 | } 211 | 212 | // 客户端向服务端发送上行消息 213 | func (c *Client) sendMsg(cmdType pb.CmdType, msg proto.Message) { 214 | // 组装顶层数据 215 | cmdMsg := &pb.Input{ 216 | Type: cmdType, 217 | Data: nil, 218 | } 219 | if msg != nil { 220 | data, err := proto.Marshal(msg) 221 | if err != nil { 222 | panic(err) 223 | } 224 | cmdMsg.Data = data 225 | } 226 | 227 | bytes, err := proto.Marshal(cmdMsg) 228 | if err != nil { 229 | panic(err) 230 | } 231 | 232 | // 发送 233 | c.sendCh <- bytes 234 | } 235 | -------------------------------------------------------------------------------- /test/ws_benchmark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | /* 8 | ------ 目标:单机群聊压测 ------ 9 | 注:本机压本机 10 | 系统:Windows 10 19045.2604 11 | CPU: 2.90 GHz AMD Ryzen 7 4800H with Radeon Graphics 12 | 内存: 16.0 GB 13 | 群成员总数: 500人 14 | 在线人数: 500人 15 | 每秒/次发送消息数量: 500条 16 | 每秒理论响应消息数量:25 0000条 = 500条 * 在线500人 17 | 发送消息次数: 40次 18 | 响应消息总量:1000 0000条 = 500条 * 在线500人 * 40次 19 | Message 表数量总数:1000 0000条 = 总数500人 * 500条 * 40次 20 | 丢失消息数量: 0条 21 | 总耗时: 39948ms 22 | 平均每500条消息发送/转发在线人员/在线人员接收总耗时: 998ms(其实更短,因为消息是每秒发一次) 23 | 24 | 如果发送消息次数为 1,时间为:940ms 25 | */ 26 | 27 | var ( 28 | // 起始phone_num 29 | pn = flag.Int64("pn", 100000, "First phone num") 30 | // 群成员总数 31 | gn = flag.Int64("gn", 500, "群成员总数") 32 | // 在线成员数量 33 | on = flag.Int64("on", 500, "在线成员数量") 34 | // 每次发送消息数量(不同在线成员发送一次,总共 500 个在线成员每人发送一次,总共发送 500 次) 35 | sn = flag.Int64("sn", 500, "每次发送消息数量") 36 | // 发送消息次数 37 | tn = flag.Int64("tn", 1, "发送消息次数") 38 | ) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | mgr := NewManager(*pn, *on, *sn, *gn, *tn) 44 | mgr.Run() 45 | 46 | select {} 47 | } 48 | -------------------------------------------------------------------------------- /test/ws_benchmark/manager.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "GoChat/pkg/util" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | const ( 16 | httpURL = "http://localhost:9090" 17 | websocketAddr = "ws://localhost:9091" 18 | contentType = "application/x-www-form-urlencoded" 19 | ) 20 | 21 | var ( 22 | msgTimer *timer 23 | isStart bool 24 | receivedMessageCount int64 25 | ) 26 | 27 | type Manager struct { 28 | clients sync.Map 29 | PhoneNum int64 // 起始电话号码 30 | OnlineCount int64 // 在线成员数量 31 | SendCount int64 // 每次发送消息数量 32 | MemberCount int64 // 群成员总数 33 | TestCount int64 // 发送消息次数 34 | chatId int64 // 群聊id 35 | tokens []string // 分配给 client 进行 Login 的 token 36 | userIds []string 37 | } 38 | 39 | func NewManager(phoneNum, onlineCount, sendCount, memberCount, testCount int64) *Manager { 40 | return &Manager{ 41 | PhoneNum: phoneNum, 42 | OnlineCount: onlineCount, 43 | SendCount: sendCount, 44 | MemberCount: memberCount, 45 | TestCount: testCount, 46 | } 47 | } 48 | 49 | func (m *Manager) Run() { 50 | // 启动计时器,每隔一段时间打印在线人数和接收消息总数 51 | m.debug() 52 | 53 | // 批量注册 MemberCount 个用户,并创建群组 54 | m.registerAndCreateGroup() 55 | fmt.Println("创建群组完成..") 56 | 57 | // 新建 websocket 连接 58 | m.batchCreate() 59 | 60 | // 循环发送消息 61 | m.loopSend() 62 | } 63 | 64 | // 每秒循环发送消息 65 | func (m *Manager) loopSend() { 66 | var ( 67 | count int64 68 | ticker = time.NewTicker(time.Second) 69 | start int64 70 | end = start + m.SendCount 71 | ) 72 | defer func() { 73 | ticker.Stop() 74 | m.clients.Range(func(k, v interface{}) bool { 75 | client, ok := v.(*Client) 76 | if ok { 77 | return true 78 | } 79 | // 主动断开 80 | client.conn.Close() 81 | return true 82 | }) 83 | }() 84 | for { 85 | select { 86 | case <-ticker.C: 87 | // 首次发送消息开始计时 88 | if !isStart { 89 | isStart = true 90 | msgTimer = newTimer(time.Second) 91 | msgTimer.run() 92 | } 93 | // 发送 SendCount 次消息 94 | for i := start; i < end; i++ { 95 | client, ok := m.clients.Load(m.userIds[i]) 96 | if !ok { 97 | continue 98 | } 99 | // 发送信息 100 | client.(*Client).send(m.chatId) 101 | } 102 | count++ 103 | if count >= m.TestCount { 104 | fmt.Println("测试完成") 105 | return 106 | } 107 | } 108 | } 109 | } 110 | 111 | // 创建在线成员数量的 websocket 连接 112 | func (m *Manager) batchCreate() { 113 | var wg sync.WaitGroup 114 | ch := make(chan struct{}, 100) 115 | for i := 0; i < int(m.OnlineCount); i++ { 116 | wg.Add(1) 117 | ch <- struct{}{} 118 | go func(i int) { 119 | defer func() { 120 | wg.Done() 121 | <-ch 122 | }() 123 | userId := m.userIds[i] 124 | token := m.tokens[i] 125 | 126 | client := NewClient(userId, token, websocketAddr) 127 | if client.conn != nil { 128 | m.clients.Store(userId, client) 129 | } 130 | }(i) 131 | } 132 | close(ch) 133 | wg.Wait() 134 | } 135 | 136 | // 批量注册用户并创建群聊 137 | func (m *Manager) registerAndCreateGroup() { 138 | var ( 139 | start = m.PhoneNum 140 | end = start + m.MemberCount 141 | ) 142 | type respStrut struct { 143 | Code int `json:"code"` 144 | Msg string `json:"msg"` 145 | Data struct { 146 | Token string `json:"token"` 147 | Id string `json:"id"` 148 | } `json:"data"` 149 | } 150 | 151 | var mutex sync.Mutex 152 | 153 | // 批量注册用户 154 | var wg sync.WaitGroup 155 | ch := make(chan struct{}, 10) // 限制并发数 156 | for i := start; i < end; i++ { 157 | ch <- struct{}{} 158 | wg.Add(1) 159 | 160 | go func(i int64) { 161 | defer func() { 162 | <-ch 163 | wg.Done() 164 | }() 165 | client := &http.Client{} 166 | 167 | // 准备要发送的表单数据 168 | data := url.Values{} 169 | data.Set("phone_number", util.Int64ToStr(i)) 170 | data.Set("nickname", "test") 171 | data.Set("password", "123") 172 | 173 | // 创建一个 POST 请求 174 | req, err := http.NewRequest("POST", httpURL+"/register", strings.NewReader(data.Encode())) 175 | if err != nil { 176 | panic(err) 177 | } 178 | req.Header.Set("Content-Type", contentType) 179 | // 发送请求并获取响应 180 | resp, err := client.Do(req) 181 | if err != nil { 182 | panic(err) 183 | } 184 | defer resp.Body.Close() 185 | 186 | // 读取数据,并解析返回值 187 | responseBody, err := ioutil.ReadAll(resp.Body) 188 | if err != nil { 189 | panic(err) 190 | } 191 | 192 | var respData respStrut 193 | err = json.Unmarshal(responseBody, &respData) 194 | if err != nil { 195 | panic(err) 196 | } 197 | 198 | mutex.Lock() 199 | m.userIds = append(m.userIds, respData.Data.Id) 200 | m.tokens = append(m.tokens, respData.Data.Token) 201 | mutex.Unlock() 202 | }(i) 203 | } 204 | close(ch) 205 | wg.Wait() 206 | 207 | if int64(len(m.tokens)) != m.MemberCount { 208 | panic("用户注册失败") 209 | } 210 | 211 | // 创建群聊 212 | // 创建一个 http.Client 213 | client := &http.Client{} 214 | 215 | // 准备要发送的表单数据 216 | data := url.Values{} 217 | data.Set("name", "benchmark_test") 218 | for _, id := range m.userIds { 219 | data.Add("ids", id) 220 | } 221 | 222 | // 创建一个 POST 请求 223 | req, err := http.NewRequest("POST", httpURL+"/group/create", strings.NewReader(data.Encode())) 224 | if err != nil { 225 | // 处理错误 226 | return 227 | } 228 | req.Header.Set("Content-Type", contentType) 229 | // 设置 token 230 | req.Header.Set("token", m.tokens[0]) 231 | 232 | // 发送请求并获取响应 233 | resp, err := client.Do(req) 234 | if err != nil { 235 | panic(err) 236 | } 237 | defer resp.Body.Close() 238 | 239 | responseBody, err := ioutil.ReadAll(resp.Body) 240 | if err != nil { 241 | panic(err) 242 | } 243 | 244 | var respData respStrut 245 | err = json.Unmarshal(responseBody, &respData) 246 | m.chatId = util.StrToInt64(respData.Data.Id) 247 | return 248 | } 249 | 250 | // 每隔 5s,打印一次 251 | func (m *Manager) debug() { 252 | go func() { 253 | allTicker := time.NewTicker(time.Second * 5) 254 | defer allTicker.Stop() 255 | for { 256 | select { 257 | case <-allTicker.C: 258 | fmt.Println("在线人数:", m.clientCount(), " 接收消息数量:", receivedMessageCount) 259 | } 260 | } 261 | }() 262 | } 263 | 264 | // 客户端总数 265 | func (m *Manager) clientCount() int { 266 | j := 0 267 | m.clients.Range(func(k, v interface{}) bool { 268 | j++ 269 | return true 270 | }) 271 | return j 272 | } 273 | -------------------------------------------------------------------------------- /test/ws_benchmark/timer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // 消息接收计时器 9 | type timer struct { 10 | id int64 11 | start time.Time 12 | end time.Time 13 | interval time.Duration 14 | } 15 | 16 | func newTimer(interval time.Duration) *timer { 17 | return &timer{interval: interval} 18 | } 19 | 20 | func (t *timer) run() { 21 | fmt.Println("开始计时") 22 | t.start = time.Now() 23 | t.end = t.start 24 | t.id = t.start.Unix() 25 | go func() { 26 | ticker := time.NewTicker(t.interval) 27 | defer ticker.Stop() 28 | for { 29 | select { 30 | case <-ticker.C: 31 | fmt.Println(t.id, " 时差(毫秒):", t.end.Sub(t.start).Milliseconds()) 32 | } 33 | } 34 | }() 35 | } 36 | 37 | // 更新 end 时间 38 | func (t *timer) updateEndTime() { 39 | t.end = time.Now() 40 | } 41 | -------------------------------------------------------------------------------- /test/ws_client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "GoChat/pkg/protocol/pb" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/gorilla/websocket" 9 | "google.golang.org/protobuf/proto" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | const ( 19 | httpAddr = "http://localhost:9090" 20 | websocketAddr = "ws://localhost:9091" 21 | ResendCountMax = 3 // 超时重传最大次数 22 | ) 23 | 24 | type Client struct { 25 | conn *websocket.Conn 26 | token string 27 | userId uint64 28 | clientId uint64 29 | clientId2Cancel map[uint64]context.CancelFunc // clientId 到 context 的映射 30 | clientId2CancelMutex sync.Mutex 31 | seq uint64 // 本地消息最大同步序列号 32 | } 33 | 34 | // websocket 客户端 35 | func main() { 36 | // http 登录,获取 token 37 | client := Login() 38 | 39 | // 连接 websocket 服务 40 | client.Start() 41 | } 42 | 43 | func (c *Client) Start() { 44 | // 连接 websocket 45 | conn, _, err := websocket.DefaultDialer.Dial(websocketAddr+"/ws", http.Header{}) 46 | if err != nil { 47 | panic(err) 48 | } 49 | c.conn = conn 50 | 51 | fmt.Println("与 websocket 建立连接") 52 | // 向 websocket 发送登录请求 53 | c.Login() 54 | 55 | // 心跳 56 | go c.Heartbeat() 57 | 58 | time.Sleep(time.Millisecond * 100) 59 | 60 | // 离线消息同步 61 | go c.Sync() 62 | 63 | // 收取消息 64 | go c.Receive() 65 | 66 | time.Sleep(time.Millisecond * 100) 67 | 68 | c.ReadLine() 69 | } 70 | 71 | // ReadLine 读取用户消息并发送 72 | func (c *Client) ReadLine() { 73 | var ( 74 | msg string 75 | receiverId uint64 76 | sessionType int8 77 | ) 78 | 79 | readLine := func(hint string, v interface{}) { 80 | fmt.Println(hint) 81 | _, err := fmt.Scanln(v) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | for { 87 | readLine("发送消息", &msg) 88 | readLine("接收人id(user_id/group_id):", &receiverId) 89 | readLine("发送消息类型(1-单聊、2-群聊):", &sessionType) 90 | message := &pb.Message{ 91 | SessionType: pb.SessionType(sessionType), 92 | ReceiverId: receiverId, 93 | SenderId: c.userId, 94 | MessageType: pb.MessageType_MT_Text, 95 | Content: []byte(msg), 96 | SendTime: time.Now().UnixMilli(), 97 | } 98 | UpMsg := &pb.UpMsg{ 99 | Msg: message, 100 | ClientId: c.GetClientId(), 101 | } 102 | 103 | c.SendMsg(pb.CmdType_CT_Message, UpMsg) 104 | 105 | // 启动超时重传 106 | ctx, cancel := context.WithCancel(context.Background()) 107 | 108 | go func(ctx context.Context) { 109 | maxRetry := ResendCountMax // 最大重试次数 110 | retryCount := 0 111 | retryInterval := time.Millisecond * 100 // 重试间隔 112 | ticker := time.NewTicker(retryInterval) 113 | defer ticker.Stop() 114 | for { 115 | select { 116 | case <-ctx.Done(): 117 | fmt.Println("收到 ACK,不再重试") 118 | return 119 | case <-ticker.C: 120 | if retryCount >= maxRetry { 121 | fmt.Println("达到最大超时次数,不再重试") 122 | // TODO 进行消息发送失败处理 123 | return 124 | } 125 | fmt.Println("消息超时 msg:", msg, ",第", retryCount+1, "次重试") 126 | c.SendMsg(pb.CmdType_CT_Message, UpMsg) 127 | retryCount++ 128 | } 129 | } 130 | }(ctx) 131 | 132 | c.clientId2CancelMutex.Lock() 133 | c.clientId2Cancel[UpMsg.ClientId] = cancel 134 | c.clientId2CancelMutex.Unlock() 135 | 136 | time.Sleep(time.Second) 137 | } 138 | } 139 | 140 | func (c *Client) Heartbeat() { 141 | // 2min 一次 142 | ticker := time.NewTicker(time.Second * 2 * 60) 143 | for range ticker.C { 144 | c.SendMsg(pb.CmdType_CT_Heartbeat, &pb.HeartbeatMsg{}) 145 | //fmt.Println("发送心跳", time.Now().Format("2006-01-02 15:04:05")) 146 | } 147 | } 148 | 149 | func (c *Client) Sync() { 150 | c.SendMsg(pb.CmdType_CT_Sync, &pb.SyncInputMsg{Seq: c.seq}) 151 | } 152 | 153 | func (c *Client) Receive() { 154 | for { 155 | _, bytes, err := c.conn.ReadMessage() 156 | if err != nil { 157 | panic(err) 158 | } 159 | c.HandlerMessage(bytes) 160 | } 161 | } 162 | 163 | // HandlerMessage 客户端消息处理 164 | func (c *Client) HandlerMessage(bytes []byte) { 165 | outputBatchMsg := new(pb.OutputBatch) 166 | err := proto.Unmarshal(bytes, outputBatchMsg) 167 | if err != nil { 168 | panic(err) 169 | } 170 | for _, output := range outputBatchMsg.Outputs { 171 | msg := new(pb.Output) 172 | err := proto.Unmarshal(output, msg) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | fmt.Println("收到顶层 OutPut 消息:", msg) 178 | 179 | switch msg.Type { 180 | case pb.CmdType_CT_Sync: 181 | syncMsg := new(pb.SyncOutputMsg) 182 | err = proto.Unmarshal(msg.Data, syncMsg) 183 | if err != nil { 184 | panic(err) 185 | } 186 | 187 | seq := c.seq 188 | for _, message := range syncMsg.Messages { 189 | fmt.Println("收到离线消息:", message) 190 | if seq < message.Seq { 191 | seq = message.Seq 192 | } 193 | } 194 | c.seq = seq 195 | // 如果还有,继续拉取 196 | if syncMsg.HasMore { 197 | c.Sync() 198 | } 199 | case pb.CmdType_CT_Message: 200 | pushMsg := new(pb.PushMsg) 201 | err = proto.Unmarshal(msg.Data, pushMsg) 202 | if err != nil { 203 | panic(err) 204 | } 205 | fmt.Printf("收到消息:%s, 发送人id:%d, 会话类型:%s, 接收时间:%s\n", pushMsg.Msg.GetContent(), pushMsg.Msg.GetSenderId(), pushMsg.Msg.SessionType, time.Now().Format("2006-01-02 15:04:05")) 206 | // 更新 seq 207 | seq := pushMsg.Msg.Seq 208 | if c.seq < seq { 209 | c.seq = seq 210 | } 211 | fmt.Println("更新 seq:", c.seq) 212 | case pb.CmdType_CT_ACK: // 收到 ACK 213 | ackMsg := new(pb.ACKMsg) 214 | err = proto.Unmarshal(msg.Data, ackMsg) 215 | if err != nil { 216 | panic(err) 217 | } 218 | 219 | switch ackMsg.Type { 220 | case pb.ACKType_AT_Login: 221 | fmt.Println("登录成功") 222 | case pb.ACKType_AT_Up: // 收到上行消息的 ACK 223 | // 取消超时重传 224 | clientId := ackMsg.ClientId 225 | c.clientId2CancelMutex.Lock() 226 | if cancel, ok := c.clientId2Cancel[clientId]; ok { 227 | // 取消超时重传 228 | cancel() 229 | delete(c.clientId2Cancel, clientId) 230 | fmt.Println("取消超时重传,clientId:", clientId) 231 | } 232 | c.clientId2CancelMutex.Unlock() 233 | // 更新客户端本地维护的 seq 234 | seq := ackMsg.Seq 235 | if c.seq < seq { 236 | c.seq = seq 237 | } 238 | } 239 | default: 240 | fmt.Println("未知消息类型") 241 | } 242 | } 243 | } 244 | 245 | // Login websocket 登录 246 | func (c *Client) Login() { 247 | fmt.Println("websocket login...") 248 | // 组装底层数据 249 | loginMsg := &pb.LoginMsg{ 250 | Token: []byte(c.token), 251 | } 252 | c.SendMsg(pb.CmdType_CT_Login, loginMsg) 253 | } 254 | 255 | // SendMsg 客户端向服务端发送上行消息 256 | func (c *Client) SendMsg(cmdType pb.CmdType, msg proto.Message) { 257 | // 组装顶层数据 258 | cmdMsg := &pb.Input{ 259 | Type: cmdType, 260 | Data: nil, 261 | } 262 | if msg != nil { 263 | data, err := proto.Marshal(msg) 264 | if err != nil { 265 | panic(err) 266 | } 267 | cmdMsg.Data = data 268 | } 269 | 270 | bytes, err := proto.Marshal(cmdMsg) 271 | if err != nil { 272 | panic(err) 273 | } 274 | 275 | // 发送 276 | err = c.conn.WriteMessage(websocket.BinaryMessage, bytes) 277 | if err != nil { 278 | panic(err) 279 | } 280 | } 281 | 282 | func (c *Client) GetClientId() uint64 { 283 | c.clientId++ 284 | return c.clientId 285 | } 286 | 287 | // Login 用户http登录获取 token 288 | func Login() *Client { 289 | // 读取 phone_number 和 password 参数 290 | var phoneNumber, password string 291 | fmt.Print("Enter phone_number: ") 292 | fmt.Scanln(&phoneNumber) 293 | fmt.Print("Enter password: ") 294 | fmt.Scanln(&password) 295 | 296 | // 创建一个 url.Values 对象,并将 phone_number 和 password 参数添加到其中 297 | data := url.Values{} 298 | data.Set("phone_number", phoneNumber) 299 | data.Set("password", password) 300 | 301 | // 向服务器发送 POST 请求 302 | resp, err := http.PostForm(httpAddr+"/login", data) 303 | if err != nil { 304 | fmt.Println("Error sending HTTP request:", err) 305 | panic(err) 306 | } 307 | defer resp.Body.Close() 308 | 309 | // 检查响应状态码 310 | if resp.StatusCode != http.StatusOK { 311 | fmt.Printf("Unexpected HTTP status code: %d\n", resp.StatusCode) 312 | panic(err) 313 | } 314 | 315 | // 读取返回数据 316 | bodyData, err := ioutil.ReadAll(resp.Body) 317 | if err != nil { 318 | panic(err) 319 | } 320 | 321 | // 获取 token 322 | var respData struct { 323 | Code int `json:"code"` 324 | Msg string `json:"msg"` 325 | Data struct { 326 | Token string `json:"token"` 327 | UserId string `json:"user_id"` 328 | } `json:"data"` 329 | } 330 | err = json.Unmarshal(bodyData, &respData) 331 | if err != nil { 332 | panic(err) 333 | } 334 | 335 | if respData.Code != 200 { 336 | panic(fmt.Sprintf("登录失败, %s", respData)) 337 | } 338 | // 获取客户端 seq 序列号 339 | var seq uint64 340 | fmt.Print("Enter seq: ") 341 | fmt.Scanln(&seq) 342 | 343 | client := &Client{ 344 | clientId2Cancel: make(map[uint64]context.CancelFunc), 345 | seq: seq, 346 | } 347 | 348 | client.token = respData.Data.Token 349 | clientStr := respData.Data.UserId 350 | client.userId, err = strconv.ParseUint(clientStr, 10, 64) 351 | if err != nil { 352 | panic(err) 353 | } 354 | 355 | fmt.Println("client code:", respData.Code, " ", respData.Msg) 356 | fmt.Println("token:", client.token, "userId", client.userId) 357 | return client 358 | } 359 | --------------------------------------------------------------------------------