aiohttp WebSocket在线用户计数器:关闭标签页计数不更新问题排查
问题分析与解决方案
我帮你梳理了代码里的几个核心问题,这些就是导致关闭标签页时计数器不更新、Firefox刷新异常的根源:
- 连接关闭的流程处理不完整:你只在WebSocket抛出错误时清理连接,但浏览器关闭标签页时会发送
CLOSE帧,这时候async for循环会自然退出,你的代码没有处理这种正常关闭的场景,导致后续的计数器递减逻辑根本没执行。 - 无效连接残留:你在
ws.prepare()之前就把连接加入列表,如果连接建立失败,无效的WebSocket对象会留在列表里,后续广播消息时会抛出异常,打断整个流程——这就是你调试时看到执行到await app['db'].decr('counter')后就卡住的原因。 - 广播消息时未处理失效连接:遍历所有WebSocket发送消息时,遇到已关闭的连接会抛出未捕获的异常,直接中断广播逻辑,导致其他客户端收不到计数器更新。
下面是修复后的完整代码,我会逐点说明修改细节:
修复后的后端代码
import asyncio import aiohttp from aiohttp import web import aioredis class CounterView(web.View): async def get(self): request = self.request app = request.app # 启用心跳机制,自动检测死连接 ws = web.WebSocketResponse(heartbeat=5.0) # 先确保连接建立成功,再加入列表 await ws.prepare(request) app['websockets'].append(ws) try: # 递增计数器并广播给所有在线客户端 count = int(await app['db'].incr('counter')) await self.broadcast_count(app, count) # 处理WebSocket消息,新增CLOSE类型的处理 async for msg in ws: if msg.type == aiohttp.WSMsgType.TEXT: await ws.send_str(msg.data) elif msg.type == aiohttp.WSMsgType.CLOSE: # 客户端主动关闭连接时,直接退出循环 break elif msg.type == aiohttp.WSMsgType.ERROR: print(f'ws连接异常关闭: {ws.exception()}') finally: # 无论连接是正常关闭还是异常退出,都会执行这里的清理逻辑 if ws in app['websockets']: app['websockets'].remove(ws) # 递减计数器并广播最新数值 count = int(await app['db'].decr('counter')) await self.broadcast_count(app, count) return ws async def broadcast_count(self, app, count): # 先过滤掉已关闭的无效连接,避免发送消息时出错 active_ws = [ws for ws in app['websockets'] if not ws.closed] for ws in active_ws: try: await ws.send_json({'msg': {'count': count}}) except Exception as e: print(f'发送消息失败: {e}') # 发送失败的连接直接从列表移除 if ws in app['websockets']: app['websockets'].remove(ws) async def init_app(loop): app = web.Application(loop=loop) db = await aioredis.create_redis('redis://localhost', loop=loop) app['db'] = db app['websockets'] = [] # 添加静态文件路由,方便直接访问index.html app.add_routes([ web.get('/', CounterView), web.static('/', './') ]) return app if __name__ == '__main__': loop = asyncio.get_event_loop() web.run_app(init_app(loop))
后端修改细节:
- 心跳机制:初始化
WebSocketResponse时设置heartbeat=5.0,服务器会每隔5秒发送ping帧,客户端回复pong,自动清理无响应的死连接。 - 连接添加时机调整:在
await ws.prepare(request)成功后才将连接加入列表,确保只有有效连接才会被管理。 - try/finally块:不管连接是正常关闭还是异常崩溃,都会执行清理逻辑,保证关闭标签页时一定会递减计数器并广播。
- 封装广播方法:新增
broadcast_count方法,先过滤已关闭的连接,再发送消息;同时捕获发送异常,避免单个无效连接打断整个广播流程。
优化后的前端代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>在线用户计数器</title> </head> <body> 当前在线人数: <span id="counter">0</span> </body> <script> window.onload = function () { const ws = new WebSocket('ws://localhost:8080'); ws.onmessage = function (event) { const data = JSON.parse(event.data); const span = document.getElementById('counter'); span.innerHTML = data.msg.count; }; // 页面关闭/刷新时主动关闭WebSocket连接 window.addEventListener('beforeunload', function() { ws.close(); }); }; </script> </html>
前端修改细节:
添加beforeunload事件监听,在页面关闭或刷新时主动关闭WebSocket连接,让服务器更快收到关闭信号,减少计数器更新的延迟。
测试效果
修改后再测试:
- 打开新标签页,所有在线标签页的计数器会同步递增;
- 关闭任意标签页,其他标签页的计数器会立即递减;
- Firefox的刷新异常问题也会消失,因为无效连接会被及时清理,计数器的增减逻辑更可靠。
内容的提问来源于stack exchange,提问作者Oleksandr Tatarinov
相关产品推荐
相关产品推荐

