Files
AI-CS/doc/WebSocket学习笔记.md
T
2025-11-17 18:05:00 +08:00

13 KiB
Raw Blame History

WebSocket 学习笔记(通俗版)

一、什么是 WebSocket

1. 类比理解

HTTP 就像"打电话"

  • 你打电话 → 对方接听 → 你说完挂断
  • 每次都要重新拨号才能通话
  • 不能一直保持连接

WebSocket 就像"对讲机"

  • 你按住按钮说话 → 对方立即听到
  • 连接一直保持,随时可以说话
  • 不需要每次都"拨号"

2. 为什么需要 WebSocket

传统 HTTP 的问题

  • 访客发送消息 → 后端收到 → 但客服不知道有新消息
  • 客服必须手动刷新页面才能看到新消息
  • 无法实时推送消息

WebSocket 的解决方案

  • 访客发送消息 → 后端收到 → 后端立即"喊话"给所有连接的客户端
  • 客服不需要刷新,消息自动显示
  • 真正的实时通信!

二、WebSocket 工作原理(整体流程)

1. 连接建立过程

1. 前端:我要连接 WebSocket(就像敲门)
   ws = new WebSocket("ws://127.0.0.1:8080/ws?conversation_id=123")

2. 后端:检查请求,同意连接(就像开门)
   → 升级 HTTP 连接为 WebSocket 连接

3. 前端:连接成功!(可以开始"对话"了)
   ws.onopen = () => { console.log("连接成功") }

关键点

  • 第一次连接还是用 HTTP(就像敲门)
  • 后端"升级"这个连接为 WebSocket(就像开门让你进来)
  • 之后就可以双向通信了

2. 消息发送和接收

前端发送消息:
  前端 → [HTTP POST] → 后端 → 保存到数据库 → 通过 WebSocket 广播

后端推送消息:
  后端 → [WebSocket] → 所有连接的客户端 → 自动显示新消息

三、后端 WebSocket 工作原理

1. Hub(中心管理器)

类比:就像"会议室管理员"

// backend/websocket/hub.go
type Hub struct {
    // 每个对话ID对应哪些客户端连接
    // 就像:会议室1有哪些人在里面
    conversations map[uint]map[*Client]bool
    
    // 注册新客户端(有人要加入)
    register chan *Client
    
    // 注销客户端(有人要离开)
    unregister chan *Client
    
    // 广播消息(有新消息要告诉所有人)
    broadcast chan *Message
}

工作流程

1. 有人连接 → 发到 register 通道
2. Hub 收到 → 把这个客户端加到对应对话的列表里
3. 有新消息 → 发到 broadcast 通道
4. Hub 收到 → 找到这个对话的所有客户端 → 发送消息给每个人

2. Client(客户端连接)

类比:就像"会议室里的一个人"

// backend/websocket/client.go
type Client struct {
    hub            *Hub           // 属于哪个 Hub
    conn           *websocket.Conn // WebSocket 连接
    send           chan *Message   // 发送消息的通道
    conversationID uint            // 属于哪个对话
}

两个重要方法

  • ReadPump():从客户端读取消息(比如心跳包)
  • WritePump():向客户端发送消息(比如新消息推送)

3. Handler(处理函数)

类比:就像"门卫",检查谁要进来

// backend/websocket/handler.go
func HandleWebSocket(hub *Hub) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 URL 获取对话ID
        conversationID := c.Query("conversation_id")
        
        // 2. 升级 HTTP 连接为 WebSocket 连接
        conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
        
        // 3. 创建客户端
        client := NewClient(hub, conn, conversationID)
        
        // 4. 注册到 Hub
        hub.register <- client
        
        // 5. 启动读写循环
        go client.WritePump()
        go client.ReadPump()
    }
}

关键步骤

  1. upgrader.Upgrade():把 HTTP 连接"升级"成 WebSocket 连接
  2. hub.register <- client:告诉 Hub "我来了"
  3. go client.WritePump():启动一个 goroutine(后台线程)来发送消息
  4. go client.ReadPump():启动一个 goroutine 来接收消息

4. 消息推送流程

// backend/service/service.go
func CreateMessage(db *gorm.DB, hub BroadcastHub) {
    // 1. 创建消息
    msg := models.Message{...}
    db.Create(&msg)
    
    // 2. 通过 WebSocket 推送
    hub.BroadcastMessage(conversationID, "new_message", msg)
}

推送过程

1. CreateMessage 调用 hub.BroadcastMessage()
2. Hub 收到消息,放到 broadcast 通道
3. Hub.Run() 循环检测到有新消息
4. 找到这个对话的所有客户端
5. 把消息发送给每个客户端(通过 client.send 通道)
6. 每个客户端的 WritePump() 收到消息,通过 WebSocket 发送给前端

四、前端 WebSocket 工作原理

1. WebSocket 客户端类

类比:就像"对讲机"

// frontend/lib/websocket.ts
class WSClient {
    private ws: WebSocket | null = null;
    private conversationId: number;
    private onMessage?: (message: WSMessage) => void;
    
    // 连接
    connect() {
        // 1. 创建 WebSocket 连接
        const wsUrl = "ws://127.0.0.1:8080/ws?conversation_id=123";
        this.ws = new WebSocket(wsUrl);
        
        // 2. 设置事件监听
        this.ws.onopen = () => {
            console.log("连接成功");
        };
        
        this.ws.onmessage = (event) => {
            // 收到消息
            const message = JSON.parse(event.data);
            if (this.onMessage) {
                this.onMessage(message);
            }
        };
        
        this.ws.onerror = (error) => {
            console.error("连接错误", error);
        };
        
        this.ws.onclose = () => {
            console.log("连接关闭");
            // 尝试重连
            this.attemptReconnect();
        };
    }
}

关键点

  • new WebSocket(url):创建连接(会自动发送 HTTP 升级请求)
  • ws.onopen:连接成功时触发
  • ws.onmessage:收到消息时触发
  • ws.onclose:连接关闭时触发

2. 在 React 组件中使用

// frontend/app/chat/page.tsx
useEffect(() => {
    if (conversationId === null) return;
    
    // 创建 WebSocket 客户端
    const wsClient = new WSClient({
        conversationId: conversationId,
        // 收到消息时的回调
        onMessage: (message) => {
            if (message.type === "new_message") {
                // 把新消息添加到消息列表
                setMessages((prevMessages) => {
                    const newMsg = message.data;
                    // 检查是否已存在(避免重复)
                    const exists = prevMessages.some((msg) => msg.id === newMsg.id);
                    if (exists) return prevMessages;
                    // 添加到列表
                    return [...prevMessages, newMsg];
                });
            }
        },
    });
    
    // 建立连接
    wsClient.connect();
    
    // 组件卸载时断开连接
    return () => {
        wsClient.disconnect();
    };
}, [conversationId]);

工作流程

  1. 页面加载时,创建 WebSocket 客户端
  2. 建立连接(自动发送请求到后端)
  3. 收到消息时,自动更新 React 状态(setMessages
  4. React 自动重新渲染,显示新消息
  5. 页面关闭时,断开连接

五、完整的消息流程(从发送到接收)

场景:访客发送消息,客服立即看到

步骤 1:访客发送消息
  前端 → [HTTP POST /messages] → 后端
  {
    conversation_id: 123,
    content: "你好",
    sender_is_agent: false
  }

步骤 2:后端处理
  后端 → 保存到数据库 → 调用 hub.BroadcastMessage()

步骤 3Hub 广播消息
  Hub → 找到对话 123 的所有客户端(访客和客服)
      → 通过 WebSocket 发送给每个客户端

步骤 4:前端接收消息
  访客端:收到消息 → 自动显示(自己发的)
  客服端:收到消息 → 自动显示(访客发的)

步骤 5:完成
  访客看到自己的消息
  客服立即看到访客的消息(无需刷新!)

六、WebSocket vs HTTP

HTTP(传统方式)

访客发送消息:
  前端 → [HTTP POST] → 后端 → 保存 → 返回成功

客服查看消息:
  前端 → [HTTP GET /messages] → 后端 → 返回所有消息
  (需要手动刷新或定时轮询)

问题

  • 客服不知道什么时候有新消息
  • 必须定时刷新(浪费资源)
  • 不是实时的

WebSocket(实时方式)

访客发送消息:
  前端 → [HTTP POST] → 后端 → 保存 → [WebSocket 推送] → 客服端

客服查看消息:
  客服端 → [WebSocket 连接] → 后端
  后端 → [有新消息时自动推送] → 客服端 → 自动显示

优势

  • 实时推送,无需刷新
  • 节省资源(不需要定时轮询)
  • 双向通信(可以前后端互相发送)

七、技术细节

1. 连接升级(Upgrade

HTTP 请求头

GET /ws?conversation_id=123 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

HTTP 响应头

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

101 状态码:表示协议切换成功(从 HTTP 切换到 WebSocket

2. 心跳检测(Ping/Pong

为什么需要心跳

  • 保持连接活跃(防止被防火墙关闭)
  • 检测连接是否断开

工作原理

后端 → [每 54 秒发送 Ping] → 前端
前端 → [收到 Ping,回复 Pong] → 后端

如果前端没有回复 Pong,后端就知道连接断开了。

3. 自动重连

// frontend/lib/websocket.ts
private attemptReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
        return; // 超过最大重试次数,停止重连
    }
    
    this.reconnectAttempts++;
    setTimeout(() => {
        this.connect(); // 重新连接
    }, 3000); // 等待 3 秒后重连
}

重连策略

  • 连接断开时自动尝试重连
  • 最多重试 5 次
  • 每次等待 3 秒

八、记忆口诀

  1. WebSocket = 对讲机:一直保持连接,随时可以说话
  2. HTTP = 打电话:每次都要重新拨号
  3. Hub = 会议室管理员:管理所有连接的人
  4. Client = 会议室里的人:每个人都有自己的连接
  5. Upgrade = 开门:把 HTTP 连接"升级"成 WebSocket
  6. Ping/Pong = 心跳:保持连接活跃
  7. Broadcast = 广播:把消息发送给所有人

九、常见问题

1. WebSocket 和 HTTP 可以同时使用吗?

可以! 就像:

  • HTTP:用来发送消息、获取列表等
  • WebSocket:用来接收实时推送

2. 为什么需要心跳检测?

防止连接被关闭

  • 有些防火墙/代理会关闭长时间不活动的连接
  • 心跳告诉它们"我还活着"

3. 如果 WebSocket 连接失败怎么办?

自动降级

  • 可以继续使用 HTTP 轮询(定时刷新)
  • 或者显示错误提示,让用户手动刷新

4. WebSocket 比 HTTP 快吗?

不一定

  • WebSocket 的优势是"实时推送",不是速度
  • 第一次建立连接需要时间
  • 但之后推送消息非常快(不需要重新建立连接)

十、代码示例总结

后端关键代码

// 1. 创建 Hub
wsHub := websocket.NewHub()
go wsHub.Run() // 启动 Hub

// 2. 注册 WebSocket 路由
r.GET("/ws", websocket.HandleWebSocket(wsHub))

// 3. 推送消息
hub.BroadcastMessage(conversationID, "new_message", msg)

前端关键代码

// 1. 创建客户端
const wsClient = new WSClient({
    conversationId: conversationId,
    onMessage: (message) => {
        // 处理收到的消息
    },
});

// 2. 建立连接
wsClient.connect();

// 3. 断开连接(组件卸载时)
return () => {
    wsClient.disconnect();
};

总结

WebSocket 就像"对讲机"

  • 建立连接后,一直保持
  • 后端可以随时"喊话"给前端
  • 前端可以随时"回复"给后端
  • 真正的实时双向通信!

我们项目的实现

  • 后端用 Hub 管理所有连接
  • 消息创建后自动推送
  • 前端自动接收并显示
  • 无需手动刷新,真正的实时体验!

记住WebSocket 不是用来替代 HTTP 的,而是用来补充 HTTP 的实时推送能力!


学习建议

  1. 先理解整体流程(连接 → 发送 → 接收)
  2. 再看代码实现(Hub、Client、Handler
  3. 运行项目,观察日志和浏览器控制台
  4. 尝试修改代码,看看效果

调试技巧

  • 打开浏览器控制台(F12),查看 WebSocket 连接状态
  • 查看后端日志,看连接和消息推送情况
  • 使用 ws.onmessage 打印收到的消息

祝你学习愉快!🚀