mirror of
https://github.com/2930134478/AI-CS.git
synced 2026-06-15 08:45:41 +08:00
13 KiB
13 KiB
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()
}
}
关键步骤:
upgrader.Upgrade():把 HTTP 连接"升级"成 WebSocket 连接hub.register <- client:告诉 Hub "我来了"go client.WritePump():启动一个 goroutine(后台线程)来发送消息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]);
工作流程:
- 页面加载时,创建 WebSocket 客户端
- 建立连接(自动发送请求到后端)
- 收到消息时,自动更新 React 状态(
setMessages) - React 自动重新渲染,显示新消息
- 页面关闭时,断开连接
五、完整的消息流程(从发送到接收)
场景:访客发送消息,客服立即看到
步骤 1:访客发送消息
前端 → [HTTP POST /messages] → 后端
{
conversation_id: 123,
content: "你好",
sender_is_agent: false
}
步骤 2:后端处理
后端 → 保存到数据库 → 调用 hub.BroadcastMessage()
步骤 3:Hub 广播消息
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 秒
八、记忆口诀
- WebSocket = 对讲机:一直保持连接,随时可以说话
- HTTP = 打电话:每次都要重新拨号
- Hub = 会议室管理员:管理所有连接的人
- Client = 会议室里的人:每个人都有自己的连接
- Upgrade = 开门:把 HTTP 连接"升级"成 WebSocket
- Ping/Pong = 心跳:保持连接活跃
- 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 的实时推送能力!
学习建议:
- 先理解整体流程(连接 → 发送 → 接收)
- 再看代码实现(Hub、Client、Handler)
- 运行项目,观察日志和浏览器控制台
- 尝试修改代码,看看效果
调试技巧:
- 打开浏览器控制台(F12),查看 WebSocket 连接状态
- 查看后端日志,看连接和消息推送情况
- 使用
ws.onmessage打印收到的消息
祝你学习愉快!🚀