这里的 server 逻辑,核心就是一句话:
维护在线连接,消费待转发消息,再把消息分发给目标客户端。
核心文件是 internal/service/chat/server.go:25。
1. Server 里有什么#
Server 结构体有 4 个关键成员:
Clients map[string]*Client按用户uuid维护在线连接表。谁在线,就能从这里找到谁。Transmit chan []byte全局消息转发队列。所有客户端发上来的消息,最后都会进这里,等待服务端处理。Login chan *Client登录队列。Logout chan *Client登出队列。
定义位置在 internal/service/chat/server.go:25。
启动时会初始化一个全局单例 ChatServer,见 internal/service/chat/server.go:35。
2. Server 怎么跑#
真正的主循环在 Start() 中,见 internal/service/chat/server.go:62。
它会一直 select 三类事件:
LoginLogoutTransmit
也就是说,这个服务端本质上就是一个事件循环。
3. 登录 / 登出逻辑#
登录事件#
登录时会做这几件事:
- 从
Login中取出client - 放进
Clients[client.Uuid] - 给这个 websocket 发一条欢迎消息
代码在 internal/service/chat/server.go:71。
登出事件#
登出时会做这几件事:
- 从
Clients中删除这个用户 - 给 websocket 发一条“已退出登录”的消息
代码在 internal/service/chat/server.go:83。
所以,Clients 这个 map 本质上就是整个系统的在线用户路由表。
4. 最核心:Transmit 消息转发逻辑#
这部分在 internal/service/chat/server.go:94。
整体流程是:
- 从
Transmit里读出一条[]byte - 反序列化成
ChatMessageRequest - 按消息类型进入不同处理分支
消息类型定义在 pkg/enum/message/message_type_enum/message_type_enum.go:3:
0:文本2:文件3:通话信令
5. 文本消息怎么处理#
文本消息逻辑在 internal/service/chat/server.go:101。
首先会做 3 件事:
- 构造
model.Message - 写数据库,状态先记为
Unsent - 根据
receive_id是用户还是群,进入不同分支
消息表结构在 internal/model/message.go:8。
单聊场景#
如果是单聊,receive_id[0] == 'U',处理流程是:
- 组装返回前端的消息体
- 如果接收方在线,就通过
Clients[receive_id]找到对方连接 - 把消息塞进对方的
SendBack - 同时给发送方自己也塞一份
SendBack
代码在 internal/service/chat/server.go:125。
这里“给发送者自己也发一份”,本质上是为了做回显。因为前端展示消息列表用的是服务端返回的标准消息结构,而不是原始请求结构,所以作者选择由后端再回推一份标准格式消息。
如果接收方不在线:
- 不会实时推送
- 但消息已经入库
- 后面用户拉取历史消息时仍然能看到
另外,这里还会尝试更新 Redis 中的消息列表缓存,见 internal/service/chat/server.go:164。
6. 群聊消息怎么转发#
群聊分支在 internal/service/chat/server.go:186。
整体逻辑是:
- 查询群信息
- 取出成员列表
members - 遍历群成员
- 在线成员才推送
- 发送者自己也回显一份
所以群聊并不是“直接广播给所有人”,而是:
遍历群成员 -> 在线的发 websocket -> 不在线的不推送
同时,这里也会更新 Redis 中的群消息缓存。
7. 文件消息逻辑#
文件消息处理在 internal/service/chat/server.go:252。
它和文本消息整体流程几乎一样,只不过字段不同:
content为空- 主要使用
url、file_name、file_type、file_size等字段
无论是单聊还是群聊,转发方式都与文本消息基本一致。
8. 通话消息逻辑#
通话消息处理在 internal/service/chat/server.go:402。
这部分和普通聊天不太一样:
- 先解析
av_data - 只有
start_call、receive_call、reject_call这几类消息会落库 - 真正的
sdp、candidate之类信令,主要做透传
转发时的规则是:
- 只发给接收方
- 不给发送方回显
代码里还专门写了注释:通话不能回显,不然会出现两个 start_call。见 internal/service/chat/server.go:468。
9. 谁负责真正写到 websocket#
Server 本身并不直接对业务消息调用 WriteMessage。
它做的事情只是把消息塞进目标客户端的 SendBack:
receiveClient.SendBack <- messageBack真正负责把消息写到 websocket 的,是 internal/service/chat/client.go:93 里的 Client.Write()。
发送成功后,Client.Write() 还会把数据库中的消息状态更新成 Sent。
所以职责分工非常清楚:
Server:决定“发给谁”Client.Write():真正执行“怎么发出去”
10. 整体链路#
你可以把整个 server 理解成下面这条链路:
- 前端通过 websocket 发送消息
Client.Read()收到后,把消息放进TransmitServer.Start()从Transmit中取消息- 先写入数据库
- 查找目标用户或群成员
- 在线用户就把消息塞进对应客户端的
SendBack - 每个客户端自己的
Write()协程,再把消息写回 websocket
11. 这套 server 的特点#
这套设计的优点是比较简单直白:
- 在线路由清晰
- 单聊 / 群聊分支明确
- 消息先落库,再推送
但缺点也比较明显:
Clients只记录在线连接,没有更完整的连接状态管理- Redis 缓存更新比较随意,只在 key 已存在时 append
- 用
len(channel)判断 channel 是否已满,不够稳妥 mutex包住 channel 发送,设计上有些别扭- 断线清理不够完整,异常退出时可能残留在线状态