这里的 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 三类事件:

  • Login
  • Logout
  • Transmit

也就是说,这个服务端本质上就是一个事件循环

3. 登录 / 登出逻辑#

登录事件#

登录时会做这几件事:

  1. Login 中取出 client
  2. 放进 Clients[client.Uuid]
  3. 给这个 websocket 发一条欢迎消息

代码在 internal/service/chat/server.go:71

登出事件#

登出时会做这几件事:

  1. Clients 中删除这个用户
  2. 给 websocket 发一条“已退出登录”的消息

代码在 internal/service/chat/server.go:83

所以,Clients 这个 map 本质上就是整个系统的在线用户路由表

4. 最核心:Transmit 消息转发逻辑#

这部分在 internal/service/chat/server.go:94

整体流程是:

  1. Transmit 里读出一条 []byte
  2. 反序列化成 ChatMessageRequest
  3. 按消息类型进入不同处理分支

消息类型定义在 pkg/enum/message/message_type_enum/message_type_enum.go:3

  • 0:文本
  • 2:文件
  • 3:通话信令

5. 文本消息怎么处理#

文本消息逻辑在 internal/service/chat/server.go:101

首先会做 3 件事:

  1. 构造 model.Message
  2. 写数据库,状态先记为 Unsent
  3. 根据 receive_id 是用户还是群,进入不同分支

消息表结构在 internal/model/message.go:8

单聊场景#

如果是单聊,receive_id[0] == 'U',处理流程是:

  1. 组装返回前端的消息体
  2. 如果接收方在线,就通过 Clients[receive_id] 找到对方连接
  3. 把消息塞进对方的 SendBack
  4. 同时给发送方自己也塞一份 SendBack

代码在 internal/service/chat/server.go:125

这里“给发送者自己也发一份”,本质上是为了做回显。因为前端展示消息列表用的是服务端返回的标准消息结构,而不是原始请求结构,所以作者选择由后端再回推一份标准格式消息。

如果接收方不在线:

  • 不会实时推送
  • 但消息已经入库
  • 后面用户拉取历史消息时仍然能看到

另外,这里还会尝试更新 Redis 中的消息列表缓存,见 internal/service/chat/server.go:164

6. 群聊消息怎么转发#

群聊分支在 internal/service/chat/server.go:186

整体逻辑是:

  1. 查询群信息
  2. 取出成员列表 members
  3. 遍历群成员
  4. 在线成员才推送
  5. 发送者自己也回显一份

所以群聊并不是“直接广播给所有人”,而是:

遍历群成员 -> 在线的发 websocket -> 不在线的不推送

同时,这里也会更新 Redis 中的群消息缓存。

7. 文件消息逻辑#

文件消息处理在 internal/service/chat/server.go:252

它和文本消息整体流程几乎一样,只不过字段不同:

  • content 为空
  • 主要使用 urlfile_namefile_typefile_size 等字段

无论是单聊还是群聊,转发方式都与文本消息基本一致。

8. 通话消息逻辑#

通话消息处理在 internal/service/chat/server.go:402

这部分和普通聊天不太一样:

  • 先解析 av_data
  • 只有 start_callreceive_callreject_call 这几类消息会落库
  • 真正的 sdpcandidate 之类信令,主要做透传

转发时的规则是:

  • 只发给接收方
  • 不给发送方回显

代码里还专门写了注释:通话不能回显,不然会出现两个 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 理解成下面这条链路:

  1. 前端通过 websocket 发送消息
  2. Client.Read() 收到后,把消息放进 Transmit
  3. Server.Start()Transmit 中取消息
  4. 先写入数据库
  5. 查找目标用户或群成员
  6. 在线用户就把消息塞进对应客户端的 SendBack
  7. 每个客户端自己的 Write() 协程,再把消息写回 websocket

11. 这套 server 的特点#

这套设计的优点是比较简单直白:

  • 在线路由清晰
  • 单聊 / 群聊分支明确
  • 消息先落库,再推送

但缺点也比较明显:

  • Clients 只记录在线连接,没有更完整的连接状态管理
  • Redis 缓存更新比较随意,只在 key 已存在时 append
  • len(channel) 判断 channel 是否已满,不够稳妥
  • mutex 包住 channel 发送,设计上有些别扭
  • 断线清理不够完整,异常退出时可能残留在线状态
本站总访问量  ·  访客数
你的IP 获取中…