├── .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 | 
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 | 群聊为例:

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 | 
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 |
--------------------------------------------------------------------------------