从轮询到全双工:第二周WebSocket通信架构与Socket.IO实战解析
从轮询到全双工:第二周WebSocket通信架构与Socket.IO实战解析
第一周把环境搭好后,第二周我们开始搞项目里最核心的部分——实时通信。对“AI心理绘画游戏”来说,得让前端的Canvas画板和后端的AI模型实时交互,不然用户画完一笔要等很久才能收到反馈,体验会很差。这周我主要研究了WebSocket协议和Socket.IO库,解决了前后端通信的一些兼容性问题。
WebSocket协议基础:解决实时传输的问题
之前做网页都是用HTTP,得客户端主动发请求服务器才会回数据。但绘画游戏不一样,服务器得主动把AI的识别结果推给前端,不然用户画完画不知道结果,没法继续玩。WebSocket就解决了这个问题:
- 握手阶段:WebSocket连接一开始是个HTTP请求,通过
Upgrade: websocket这个头把协议升级成WebSocket,这样就能穿过大部分防火墙和代理。 - 全双工通信:连接建立后,客户端和服务器可以同时发数据,不用等对方发完才能发。比如用户画一笔,前端马上把笔迹发给服务器,服务器也能同时把AI的识别结果推回来。
- 帧格式:数据会分成很小的帧传输,不管是二进制还是文本,头部都很小,不会像HTTP那样带一堆没用的头信息,节省带宽。
Socket.IO:让WebSocket更稳定好用
原生WebSocket虽然能解决实时通信的问题,但在实际网络环境下很容易出问题,比如网络波动一下就会断开,而且有些老网络环境不支持WebSocket。Socket.IO在WebSocket基础上做了封装,帮我们解决了这些麻烦:
- 自动重连:如果网络断了,原生WebSocket会直接报错,而Socket.IO会自动尝试重新连接,连上之后还能接着传数据,不会让用户画了一半的画丢失。
- 降级策略:要是用户的网络不支持WebSocket,Socket.IO会自动切换成HTTP长轮询,这样就算在老网络环境下,游戏也能正常运行,不会出现连不上服务器的情况。
- 房间功能:我们游戏里每个房间是一个独立的游戏局,Socket.IO的房间功能刚好能用来隔离不同房间的用户,比如房间A的用户画的画,只会推给房间A的其他人,不会干扰房间B。
前后端Socket.IO的事件匹配
刚开始对接的时候,我们遇到了一个大问题:前端发的事件,后端收不到;后端发的事件,前端也没反应。后来才发现是事件名没对上。
比如前端用 socket.emit('drawing_update', data) 发画笔数据,后端得用 @socketio.on('drawing_update') 来接收;后端用 emit('drawing_sync', result) 推绘画同步数据,前端得用 socket.on('drawing_sync', callback) 来监听。一开始我们前端写的事件名和后端不匹配,导致数据一直传不过去,调了半天才发现是这个问题。
后来我们定了个规则:所有事件名都用小写加下划线,比如 join_room、drawing_update、submit_guess,前后端对照着写,再也没出过错。
用浏览器DevTools调试WebSocket
之前不知道怎么查看WebSocket的通信数据,后来发现浏览器的DevTools里有个Network面板,点开WS就能看到WebSocket的通信记录。
比如用户画一笔,前端会发一个 drawing_update 事件,里面带着笔迹的坐标数据,在DevTools里能看到这个事件的payload,是一个JSON对象,里面有笔触的点数据、颜色、粗细这些信息。后端收到后处理完,会发一个 drawing_sync 事件,payload里是同样的笔触数据,推给房间里的其他用户。
通过DevTools,我们能清楚看到数据有没有发出去,有没有收到,要是数据不对,还能直接看payload里的内容,比打印日志方便多了。
消息格式分析:数据到底长什么样
我们传输的数据都是JSON格式的,这样前后端都能方便解析。比如前端发的画笔数据是这样的:
{
"room_id": "room1",
"user_id": "user123",
"stroke_data": {
"points": [[100, 200], [105, 205]],
"color": "#000000",
"width": 2
}
}
后端发的绘画同步数据是这样的:
{
"user_id": "user123",
"stroke_data": {
"points": [[100, 200], [105, 205]],
"color": "#000000",
"width": 2
}
}
前端发的完成绘画数据是这样的:
{
"room_id": "room1",
"user_id": "user123",
"image_base64": "Base64编码的图片",
"drawing_time_ms": 5000,
"behavior_data": {
"drawing_duration_ms": 5000,
"stroke_count": 10,
"eraser_usage": 0
}
}
后端发的AI识别结果会在回合结束时通过 round_result 事件推送给所有用户:
{
"round_number": 1,
"target_word": "苹果",
"result_type": "human_win",
"ai_guess": "苹果",
"ai_confidence": 0.9,
"ai_correct": true,
"human_correct": true,
"human_winner_id": "user123",
"guesses": [
{
"nickname": "玩家1",
"guess": "苹果",
"correct": true
}
]
}
一开始我们在设计数据结构时考虑不够周全,导致后端解析的时候老是出错,后来仔细设计了JSON结构,解析起来就方便多了,而且数据结构清晰,不容易搞错。
遇到的难点和解决办法
这周最大的难点就是前后端通信不稳定,有时候能收到数据,有时候收不到。后来发现是因为Socket.IO的连接没处理好,前端页面刷新的时候,旧的连接没断开,新的连接又建立了,导致服务器同时收到两个连接的数据,就乱了。
解决办法是在前端页面卸载的时候,确保Socket连接正常关闭。另外,我们还遇到了一个关键问题:猜词阶段结束时的重复触发问题。当所有玩家都提交了猜测后,后端会自动结束猜词阶段,但同时前端的计时器也会触发结束事件,导致后端收到两次结束请求,引起逻辑混乱。
我们的解决办法是在后端的 _do_end_guessing 函数中添加了防重复触发机制,通过 round_ended 标记来确保每个回合只结束一次:
# 防止重复触发(submit_guess 和超时可能同时到达)
if state.get('round_ended'):
return
state['round_ended'] = True
另外,我们还优化了AI猜词的处理逻辑,在结束猜词阶段时等待AI结果最多3秒,确保AI有足够的时间完成识别:
# 等待AI结果(最多等3秒,AI可能还在调用中)
round_info = None
for _ in range(15): # 15 × 0.2s = 3s
round_info = models.get_round(round_id)
if round_info and round_info.get('ai_guess'):
break
time.sleep(0.2)
经过这周的调试,我们的实时通信功能终于稳定了,用户画一笔,后端能马上收到并同步给其他用户,AI识别结果也能及时推回来,游戏体验好了很多。下周我们打算开始深入研究AI识别模块,看看怎么把画板上的画传给模型进行更准确的识别。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)