Files
EchoChat/backend/go-service/pkg/ws/client.go
bujinyuan 2c59500e27 fix(phase2a): 代码审查修复 - 8项关键/重要问题
安全修复:
- WebSocket Token 增加 Redis 有效性校验(已登出用户无法建立 WS)

功能修复:
- GetRecommendFriends 改为批量查询,正确返回用户名/昵称/头像
- 上下线通知:OnlineService 通过接口注入获取好友列表推送状态变更
- 管理端在线用户 API 补充用户名信息

代码质量:
- 所有 json.Marshal/Redis 错误增加检查与日志
- ContactController 13 个 endpoint 统一走 handleError 业务错误映射
- 管理端 Controller 补全包注释、函数注释和结构化日志
- 前端 5 个联系人页面 avatar 工具函数抽取到 utils/avatar.js

Made-with: Cursor
2026-03-02 18:48:28 +08:00

150 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ws
import (
"encoding/json"
"sync/atomic"
"time"
"github.com/echochat/backend/pkg/logs"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
const (
writeWait = 10 * time.Second // 写超时
pongWait = 60 * time.Second // 等待 pong 的超时
pingPeriod = 30 * time.Second // 心跳发送间隔(必须小于 pongWait
maxMessageSize = 4096 // 单条消息最大字节数
sendBufSize = 256 // 发送缓冲区大小
)
// DisconnectHandler 断线回调函数类型
type DisconnectHandler func(userID int64)
// Client 封装单个 WebSocket 客户端连接
// 每个连接持有两个 goroutinereadPump读取客户端消息和 writePump写入消息到客户端
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte // 待发送消息缓冲队列
UserID int64 // 关联的用户 ID
onDisconnect DisconnectHandler // 断线回调(清理在线状态等)
closedByHub int32 // 原子标记:是否被 Hub 主动踢出(用于避免重复连接时的竞态清理)
}
// NewClient 创建客户端实例
func NewClient(hub *Hub, conn *websocket.Conn, userID int64) *Client {
return &Client{
hub: hub,
conn: conn,
send: make(chan []byte, sendBufSize),
UserID: userID,
}
}
// SetOnDisconnect 设置断线回调
func (c *Client) SetOnDisconnect(handler DisconnectHandler) {
c.onDisconnect = handler
}
// SetClosedByHub 标记此连接被 Hub 主动关闭(重复连接踢出场景)
// 线程安全,使用 atomic 操作
func (c *Client) SetClosedByHub() {
atomic.StoreInt32(&c.closedByHub, 1)
}
// IsClosedByHub 检查此连接是否被 Hub 主动关闭
// 断线回调中使用此方法判断是否需要执行下线清理
func (c *Client) IsClosedByHub() bool {
return atomic.LoadInt32(&c.closedByHub) == 1
}
// MessageHandler 消息处理回调函数类型
type MessageHandler func(client *Client, msg *Message)
// ReadPump 读取客户端消息的循环
// 当连接断开或出错时退出,并触发注销流程
func (c *Client) ReadPump(onMessage MessageHandler) {
defer func() {
c.hub.Unregister(c)
c.conn.Close()
if c.onDisconnect != nil {
c.onDisconnect(c.UserID)
}
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, rawMsg, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
logs.Warn(nil, "ws.client.ReadPump", "WebSocket 异常关闭",
zap.Int64("user_id", c.UserID), zap.Error(err))
}
return
}
var msg Message
if err := json.Unmarshal(rawMsg, &msg); err != nil {
logs.Warn(nil, "ws.client.ReadPump", "消息格式解析失败",
zap.Int64("user_id", c.UserID), zap.Error(err))
continue
}
if onMessage != nil {
onMessage(c, &msg)
}
}
}
// WritePump 向客户端写入消息的循环
// 从 send channel 读取消息并写入 WebSocket同时负责心跳
func (c *Client) WritePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
logs.Warn(nil, "ws.client.WritePump", "写入消息失败",
zap.Int64("user_id", c.UserID), zap.Error(err))
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// Send 向客户端发送消息(非阻塞,缓冲区满时丢弃)
func (c *Client) Send(data []byte) bool {
select {
case c.send <- data:
return true
default:
logs.Warn(nil, "ws.client.Send", "发送缓冲区已满,丢弃消息",
zap.Int64("user_id", c.UserID))
return false
}
}