websocket服务端必须实施双层限流:nginx层按ip限制并发连接数,应用层对每个连接独立限速并设置心跳超时。前端控制不可信,须防绕过、防伪造、防资源耗尽。

WebSocket服务端必须做连接限流,前端控制完全不可信
浏览器对同一域名的 WebSocket 并发连接数天然限制在 6~10 个,但这只是客户端软限制,攻击者可绕过浏览器直接用 curl、ws CLI 工具或自写脚本发起海量连接。服务端若不主动限流,ulimit -n 达到系统默认 1024 后会触发 EMFILE 错误,新连接被内核直接拒绝,整个服务陷入半瘫痪。
- 永远不要依赖
new WebSocket()前的 JS 判断或 localStorage 计数——用户禁用 JS 或篡改代码后形同虚设 - 限流策略必须在服务端入口层(如 Nginx)和服务逻辑层(如 Go 的
gorilla/websocket)双落地 - 按真实客户端 IP 限流时,需通过
X-Forwarded-For或CF-Connecting-IP提取(注意防伪造,配合real_ip_header配置)
Nginx 层用 limit_conn 做第一道连接闸门
Nginx 是最轻量、最有效的前置连接过滤器,它能在握手请求(HTTP Upgrade)阶段就拦截超额连接,避免请求抵达应用进程浪费资源。
- 配置示例:
map $http_x_forwarded_for $client_real_ip { ~^(?<ip>\d+\.\d+\.\d+\.\d+) $ip; default $remote_addr; } limit_conn_zone $client_real_ip zone=perip:10m; server { location /ws { limit_conn perip 5; # 单 IP 最多 5 个并发连接 proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }</ip> - 注意
limit_conn统计的是当前活跃连接数,不是请求数;它对已建立的长连接持续生效 - 若使用 Cloudflare,需在 Nginx 中信任
CF-Connecting-IP头,并在real_ip_header和set_real_ip_from中配置可信代理段
应用层用滑动窗口对每个连接独立限速
连接建立后,恶意客户端可能发送高频小消息(如每秒百条 ping),耗尽 CPU。此时需为每个 *websocket.Conn 分配独立计数器,而非全局共享——否则正常用户会被误杀。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 以
gorilla/websocket为例,在conn.ReadMessage()前检查:type connLimiter struct { limiter *rate.Limiter // 每秒最多 3 条消息 } func (c *connLimiter) allow() bool { return c.limiter.Allow() } // 使用:if !limiter.allow() { conn.Close() } - 推荐参数:
rate.Every(333 * time.Millisecond)(≈3 QPS),超限直接conn.Close(),不返回错误帧——防止被用于探测 - 避免用 Redis 等外部存储做计数器,网络延迟会放大性能损耗;内存内
rate.Limiter或golang.org/x/time/rate足够可靠
空闲连接必须设置 ping/pong 超时并主动断开
攻击者常建大量“僵尸连接”:握手成功后不发任何业务消息,只维持 TCP 连接,缓慢耗尽服务端文件描述符和内存。这类连接无法靠限流捕获,必须靠心跳机制识别。
-
gorilla/websocket中设置:conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(30 * time.Second)) return nil }) conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 关键点:每次收到
Pong后重置ReadDeadline;若超时未收到任何帧(包括Ping、业务消息),ReadMessage()返回io.EOF或net.OpError,此时应显式conn.Close() - 不要依赖浏览器自动发
Ping——很多客户端库不实现,或间隔长达 2 分钟;服务端应主动发Ping(用SetPingHandler+ 定时器),但需注意避免对低功耗设备造成压力
真实防御链条里,Nginx 的 limit_conn 和应用层的 per-connection rate limiter 必须同时启用,且超时时间要错开(比如 Nginx 设 5 秒,应用层设 30 秒),否则容易出现“连接刚被 Nginx 放行,立刻被应用层因超时关闭”的抖动现象。










