You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

aiohttp WebSocket在线用户计数器:关闭标签页计数不更新问题排查

问题分析与解决方案

我帮你梳理了代码里的几个核心问题,这些就是导致关闭标签页时计数器不更新、Firefox刷新异常的根源:

  1. 连接关闭的流程处理不完整:你只在WebSocket抛出错误时清理连接,但浏览器关闭标签页时会发送CLOSE帧,这时候async for循环会自然退出,你的代码没有处理这种正常关闭的场景,导致后续的计数器递减逻辑根本没执行。
  2. 无效连接残留:你在ws.prepare()之前就把连接加入列表,如果连接建立失败,无效的WebSocket对象会留在列表里,后续广播消息时会抛出异常,打断整个流程——这就是你调试时看到执行到await app['db'].decr('counter')后就卡住的原因。
  3. 广播消息时未处理失效连接:遍历所有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:32:03