Computer NetworkNotes

第 18 章:WebSocket 与长连接

zjc 于 2026-01-18 发布

这是《计算机网络 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 HTTP 是请求响应模型,服务端主动推送并不自然。WebSocket 通过一次 HTTP Upgrade 建立全双工长连接,适合聊天、协同编辑、实时行情、游戏和推送。长连接的价值是降低建连成本,挑战是连接治理、心跳、断线重连、广播扇出和故障切换。

18.1 从 HTTP 到 WebSocket

握手请求:

GET /ws HTTP/1.1
Host: ws.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://www.example.com

握手响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手完成后,连接不再使用 HTTP 请求响应语义,改用 WebSocket 帧。

18.2 帧格式

WebSocket 消息由帧组成:

Opcode 含义
0x1 文本帧
0x2 二进制帧
0x8 连接关闭
0x9 Ping
0xA Pong
0x0 continuation

特性:

  1. 支持文本和二进制;
  2. 支持消息分片;
  3. 支持控制帧;
  4. 客户端到服务端的帧必须掩码;
  5. 有消息边界;
  6. 默认使用 TCP 字节流承载。

18.3 使用 wscat

npm install -g wscat
wscat -c ws://127.0.0.1:8080/ws
wscat -c wss://ws.example.com/ws

发送消息后可观察服务端日志和帧解析。调试时注意 URL 协议是 wswss,不是 http

18.4 心跳与假死

长连接可能被以下设备悄悄断开:

  1. 负载均衡空闲超时;
  2. NAT 超时;
  3. 防火墙会话超时;
  4. 代理连接池回收;
  5. 服务端主动关闭。

策略:

客户端每 25 秒发送 Ping
服务端收到后回 Pong
服务端读空闲 60 秒触发关闭
客户端连续 2 次未收到 Pong 则重连

心跳间隔必须小于链路中最短空闲超时。

18.5 断线重连

客户端重连要求:

  1. 指数退避加随机抖动;
  2. 最大重连间隔;
  3. 会话恢复令牌;
  4. 消息序号或版本号;
  5. 补拉离线消息;
  6. 去重;
  7. 网络切换后快速重试;
  8. 服务端可下发重试建议。

错误示例:

断线后每 100ms 重连
  -> 服务端尚未恢复
     -> 连接风暴
        -> 雪崩

正确示例:

1s、2s、4s、8s,最大 60s,加 0-500ms 随机抖动

18.6 连接治理

服务端要治理:

项目 建议
单机连接数 设置上限和监控
单 IP 连接数 限流,防滥用
用户连接数 同一用户多端策略
消息大小 限制帧和消息上限
发送队列 控制慢消费者
空闲时间 心跳和读超时
广播扇出 分组和分片
优雅关闭 先停止接收,再排空

慢消费者问题:

客户端接收慢
  -> 服务端为它积压消息
     -> 内存上涨
        -> 影响其他连接

处理:

  1. 每连接发送队列上限;
  2. 超限断开;
  3. 关键消息和可丢消息分级;
  4. 广播降级为最新值;
  5. 使用有界队列;
  6. 监控队列长度。

18.7 Spring Boot 示例

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(
            WebSocketHandlerRegistry registry) {
        registry.addHandler(new EchoHandler(), "/ws")
                .setAllowedOrigins("https://www.example.com");
    }
}

处理器:

public class EchoHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(
            WebSocketSession session,
            TextMessage message) throws Exception {
        session.sendMessage(
            new TextMessage("ack:" + message.getPayload()));
    }
}

生产实现还需心跳、异常处理、连接统计和限流。

18.8 集群与广播

多节点长连接服务需要跨节点推送:

用户 A 连接 Node 1
用户 B 连接 Node 2
系统事件发布到 Kafka / Redis / MQ
  -> 所有节点收到
     -> 各节点推给本机连接

要点:

  1. 连接路由表;
  2. 用户到节点映射;
  3. 消息可靠性和顺序;
  4. 重复消息去重;
  5. 节点故障切换;
  6. 广播风暴控制;
  7. 离线消息存储;
  8. 监控端到端延迟。

18.9 代理与负载均衡

反向代理必须支持 Upgrade:

location /ws {
    proxy_pass http://websocket-backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

注意:

  1. proxy_read_timeout 应大于心跳间隔;
  2. LB 空闲超时要同步调整;
  3. TLS 使用 wss://
  4. 保持客户端真实 IP;
  5. 健康检查不能只看端口;
  6. 会话粘滞视业务状态而定。

18.10 安全

  1. 强制 wss://
  2. 校验 Origin;
  3. 握手时完成认证;
  4. 短期 token 或 Ticket;
  5. 限制消息大小;
  6. 防止注入和 XSS;
  7. 每次业务消息授权;
  8. 不信任客户端上报的设备信息;
  9. 监控异常连接和消息频率。

本章小结

WebSocket 通过 HTTP Upgrade 建立全双工长连接,使用帧协议承载文本和二进制消息。长连接治理重点是心跳、断线重连、连接上限、慢消费者和集群广播。反向代理和 LB 必须支持 Upgrade 并保证超时大于心跳间隔,安全上应使用 wss、校验 Origin 并在握手和业务消息两层授权。

思考题

  1. WebSocket 握手为什么返回 101?
  2. 心跳间隔和 LB 空闲超时应满足什么关系?
  3. 如何避免断线重连风暴?
  4. 慢消费者为什么可能拖垮服务?
  5. 设计一个百万连接推送系统的架构。