websocket限流需分层实施:nginx在握手阶段用limit_req限制单位时间升级请求;服务端在.upgrade回调中结合redis做ip级握手频控和客户端id去重;连接建立后为每连接配独立令牌桶控消息速率;redis计数须用lua保证incr+expire原子性。
直接限制 websocket 连接频率,不能靠 http 层的通用限流中间件(比如 webapithrottle 或 throttlerequestfilter),因为 websocket 握手后就脱离了请求-响应生命周期——它是一次升级、长期存活的连接。真要防攻击,得在协议握手阶段拦截,或在连接建立后做会话级行为控制。
WebSocket 握手阶段必须拦截高频连接请求
攻击者常通过脚本快速发起大量 new WebSocket('ws://...'),哪怕每个连接只存活几秒,也能耗尽服务端文件描述符或内存。关键不是“连接数上限”,而是“单位时间内允许多少次握手”。
- 在 Nginx 层用
limit_req针对Upgrade: websocket请求限速,例如匹配location /ws { ... limit_req zone=wsburst burst=5 nodelay; } - 若用 uWebSockets,
.upgrade回调里可读取req->getHeader("Sec-WebSocket-Key")和 IP,结合 Redis 计数器判断 60 秒内该 IP 是否超过 10 次握手;超限则直接res->writeStatus("429")->end(); - 注意:不要在
.open回调里做频率检查——此时连接已建立,资源已被占用,防御滞后
单客户端重复连接必须绑定会话标识
无登录场景下,用户刷新页面或误点重连,会导致同一浏览器标签页出现多个活跃连接,造成状态错乱或资源泄漏。这不是“攻击”,但危害等同。
- 前端首次建连时生成持久化
localStorageID(如crypto.randomUUID()),每次连接都带上Sec-WebSocket-Protocol: client-id-xxx头 - 服务端在
.upgrade中解析该头,查 Redis:GET ws:client:<code>xxx;若存在旧连接 ID,则主动ws->close(1008, "Duplicate session")并覆盖新值 - 务必设置 Redis key 过期时间(如
EXPIRE ws:client:xxx 3600),避免僵尸 ID 积压
连接建立后仍需消息级限流
即使握手被控住,恶意客户端仍可能在单个连接内疯狂发 ping/pong 或业务消息(如高频下注、刷弹幕)。这时得靠漏桶或令牌桶实时控速。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- uWebSockets 可在
.message回调中为每个ws实例挂一个TokenBucket对象,每收到一条消息消耗 1 token,token 补充速率为 5/s - PHP Ratchet 用户别用
$conn->send()直接广播,先走if (!$this->bucket->tryConsume()) { $conn->close(); return; } - 避免全局共享桶(如所有连接共用一个计数器),否则会误杀正常用户;每个连接独立桶是底线
Redis 计数器必须带原子性与过期保障
几乎所有方案都依赖 Redis 做跨进程计数,但容易踩两个坑:没用 INCR+EXPIRE 原子组合,或 key 过期策略写错导致计数永久残留。
- 正确写法(Lua 脚本):
EVAL "local c = redis.call('incr', KEYS[1]); if c == 1 then redis.call('expire', KEYS[1], ARGV[1]) end; return c" 1 ws:ip:1.2.3.4 60 - 别用
SETNX + EXPIRE分两步——中间若崩溃,key 就没过期时间 - 测试时用
redis-cli --scan --pattern "ws:*"定期清理残留 key,生产环境建议加 TTL 监控告警
真正难的不是写限流逻辑,而是确认每个环节的“作用域”:Nginx 控的是入口请求,服务端控的是连接生命周期,Redis 控的是跨实例状态——三者漏掉任何一层,攻击者就能绕过去。










