Skip to content

WebSocket 原理大揭秘

一、WebSocket是什么

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的交互数据变得更为简单,允许服务端主动向客户端推送数据。在维基百科中提到,WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性连接,并进行双向数据传输。 其实WebSocket用于http协议在持久通信上的能力不足,本质上是一种计算机网络应用层协议

特点

  • 建立在TCP协议之上,服务端的实现比较容易
  • 与 HTTP 协议有这良好的兼容性,默认端口443和80,握手阶段也采用http协议,因此握手时不容易被屏蔽,也就是意味着我们能通过HTTP代理服务器
  • 数据格式比较轻量,性能开销小
  • 可以发送二进制数据和文本(下篇文章我将会对其进行一个详细解析)
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符是ws(加密后:wss),服务器网址就是URL

二、WebSocket必要性

HTTP协议的通信只能由客户端发起,不具备服务器推送能力 就比如在http协议下,我们想获取此刻的实时数据,就只能是客户端向服务器发出请求,服务器返回查询结果。HTTP协议做不到服务器主动向客户端推送信息 如果数据不存在连续的状态变化可能还好些,但如果服务器的状态变化是连续的,那么客户端要获取信息就非常麻烦,就意味着我们只能使用轮询,即每个一段时间就发出询问,了解服务器是否有最新的信息。但轮询的效率低,非常浪费资源————服务端被迫维持来自每个客户端的大量不同的链接,大量的轮询请求会造成无用的数据传输(带上多余的header。 虽说http协议本身是没有持久通信能力的,为了解决这个问题,因此就有了WebSocket.

(注:在我的另一个项目中URL Hash传递就是使用了轮询,详情请搜索:URL Hash)

三、WebSocket 和 HTTP 的区别

  • 共性:都是基于TCP的可靠传输协议,都是应用层协议
  • 联系:WebSocket建立握手时通过HTTP传输的,但是建立之后就不需要HTTP

注:HTTP/2只向服务器推送静态资源,无法推送指定信息

三、WebSocket原理

Websocket协议也需要通过已建立的TCP链接来传输数据,这与http相一致。但WebSocket协议和Http是有一定的交叉关系。 WebSocket相对于http是一个持久化协议 HTTP的生命周期通过Request来界定,也就是一个HTTP连接中可以发送多个Request,接收多个Response。在HTTP中Request = Response。也就是一个Request只能由一个Response,并且Response只能是被动的,不能主动发起。 先看一个典型的 WebSocket 握手:

js
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

不同点:

js
Upgrade: WebSocket
Connection: Upgrade

你可能发现这段类似于HTTP的请求中多了UpgradeConnection这两个字段。其实这就是WebSocket的核心了,告知:Apache、Nginx服务器这里是要用Websocket协议,而不是HTTP。


现在来解析一下里面的一些内容吧

js
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
  • Sec-WebSocket-key :一个 Base64 encode 的值,这个是浏览器随机生成,这里为了让服务器验证是不是真的是WebSocket助理,确保代理服务器不会缓存WebSocket握手,为每个连接提供唯一的标识
  • Sec-WebSocket-Protocol:用户定义的字符串,用于区分同URL下,不同的服务所需要的协议。
  • Sec-WebSocket-Version:指定服务器所使用的 WebSocket Draft 版本,最初WebSocket还在Draft阶段,不同的协议......

握手成功后,服务器会返回以下内容,表明已经接收到请求,也就是成功建立WebSocket啦~

js
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

这俩个字段依然是固定的,也就是告诉客户端即将升级WebSocket协议

js
Upgrade: WebSocket
Connection: Upgrade
  • Sec-WebSocket-Accept:经过服务器确认并且加密过后的 Sec-WebSocket-Key。
  • Sec-WebSocket-Protocol:最终使用协议

js
HTTP/1.1 101 Switching Protocols

状态码101表示服务器已理解并接受客户端的协议升级请求

握手成功后TCP连接保持开放,后续通信使用WebSocket帧格式,不再包含HTTP头,显著减少数据传输开销

综上所述,WebSocket连接的过程为:客户端发起http请求,经过三次握手后,建立起TCP连接;http请求中存放WebSocket支持的版本号信息,eg: Upgrade、Connectin、Sec-WebSocket-Key......

其实,在部分古老的浏览器中并不能支持WebSocket,这也是他为数不多的缺点

四、断线重连与心跳机制

心跳机制顾名思义就是客户端定时给服务端发送消息,证明客户端是在线的,如果超过一定的时间没有发送就是离线了。

如何判断客户端是在线离线?

第一次:当客户端发送请求至服务端时会携带唯一标识、时间戳,服务端到db或者缓存去查询请求的唯一标识,如果不存在就存入db或者缓存中。 第二次:客户端定时再次发送请求携带唯一标识与时间戳,去db或缓存中查询,如果存在则把上次的时间戳拿出,使用当前时间减去上次的时间。若得到的毫秒数大于指定的时间则离线,小于就是在线

解决断线

这里我们了解到nginx代理的 WebSocket 转发,无消息连接会出现超时断开的问题。这里有两种解决方案:①修改nginx配置信息,②WebSocket发送心跳包 我们这里就主要讲解一下心跳包吧 WebSocket超时没有消息,自动断开连接 这时候我们要知道服务端设置的超时时长是多少,在小于超时时间内发送心跳包

  • 客户端主动发送上行心跳包
  • 服务端主动发送下行心跳包

那我们又该如何实现心跳包呢? 这要从机制开始说起,心跳包一般都是很小的包,或者是只包含包头的空包,就是用于告诉服务器客户端还在线,也是作为保持长连接的手段 在TCP机制中,本身是存在心跳包机制的,也就是TCP选项:SO_KEEPALIVE。系统默认设置的2小时的心跳评率,但是他检查不到一些硬件的断线以及防火墙的问题。并且逻辑层处理断线也存在一定的困难,一般我们将心跳包用于长连接与保活 心跳包一般来说都是在逻辑层发送空的echo包来实现的:下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。 具体的心跳检测:

    1. 客户端每隔一个间隔发送探测包给服务器
    1. 客户端发包时启动一个超时定时器
    1. 服务器端接收到检测包,回应一个包
    1. 客户端及收到服务器的心跳包,服务器正常,删除超时定时器
    1. 若客户端的超时定时器超时,依然没有收到应答包,则说明服务器有问题

WebSocket异常 此时需要客户端用onclose关闭连接,服务端上线前要清除之前的数据,否则只要请求服务端的都会被视为离线。 此时就需要我们处理重连,这里引入reconnecting-WebSocket.min.js

js
var ws = new ReconnectingWebSocket(url);
// 断线重连:
reconnectSocket(){
    if ('ws' in window) {
        ws = new ReconnectingWebSocket(url);
    } else if ('MozWebSocket' in window) {
       ws = new MozWebSocket(url);
    } else {
      ws = new SockJS(url);
    }
}

断网监测则使用 offline.min.js

js
onLineCheck(){
    Offline.check();
    console.log(Offline.state,'---Offline.state');
    console.log(this.socketStatus,'---this.socketStatus');

    if(!this.socketStatus){
        console.log('网络连接已断开!');
        if(Offline.state === 'up' && WebSocket.reconnectAttempts > WebSocket.maxReconnectInterval){
            window.location.reload();
        }
        reconnectSocket();
    }else{
        console.log('网络连接成功!');
        WebSocket.send("heartBeat");
    }
}

// 使用:在WebSocket断开链接时调用网络中断监测
WebSocket.onclose => () {
    onLineCheck();
};

五、反向代理时的WebSocket配置

首先,WebSocket作为一种升级的HTTP协议,在反向代理环境中需要特殊处理,主要原因是:

  • 连接升级机制:WebSocket通过HTTP升级机制建立,需要正确传递Upgrade头
  • 长连接特性:与普通HTTP请求不同,WebSocket建立后长时间保持连接
  • 实时传输需求:数据需要实时传递,不能被过度缓冲 Nginx关键配置详解:
nginx
location /ws/ {
    # 1. 必须配置的WebSocket升级头
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
    # 2. 延长超时时间,防止长连接被中断
    proxy_read_timeout 3600s;
    
    # 3. 关闭缓冲,保证实时性
    proxy_buffering off;
    
    # 4. 确保正确的负载均衡策略
    proxy_pass http://websocket_backend;
}

upstream websocket_backend {
    # 5. 使用IP哈希确保连接一致性
    ip_hash;
    server backend1:8080;
    server backend2:8080;
}

配置思维图解:

graph TD
    A[WebSocket反向代理配置] --> B[协议升级配置]
    A --> C[连接维护配置]
    A --> D[实时传输配置]
    A --> E[负载均衡配置]
    
    B --> B1[proxy_set_header Upgrade]
    B --> B2[proxy_set_header Connection]
    
    C --> C1[proxy_read_timeout]
    C --> C2[proxy_send_timeout]
    
    D --> D1[proxy_buffering off]
    
    E --> E1[ip_hash/sticky sessions]
    E --> E2[upstream配置]
    
    %% 典型问题与解决方案
    F[常见问题] --> F1[连接无法建立]
    F --> F2[连接意外断开]
    F --> F3[消息延迟]
    
    F1 --> B
    F2 --> C
    F3 --> D

实际应用中的注意事项

在实际项目中,我曾遇到这些常见问题及其解决方案:

  • WebSocket连接无法建立:多数情况是因为没有正确配置Upgrade和Connection头
  • 连接频繁断开:通常是因为proxy_read_timeout设置过短,应设置为心跳间隔的3-4倍
  • 负载均衡导致连接问题:必须使用会话粘性(如ip_hash)确保客户端总是连接到同一后端服务器
  • 高并发环境性能问题:建议使用TCP配置模式而非HTTP模式
flowchart TD
    A[WebSocket事件回调] --> B{是否需要更新UI?}
    B -->|是| C[优化状态更新]
    B -->|否| D[避免组件重渲染]
    
    C --> C1[批量更新]
    C --> C2[防抖/节流]
    C --> C3[状态归一化]
    
    D --> D1[useRef存储]
    D --> D2[状态隔离]
    D --> D3[消息缓冲]
    
    C1 --> E[优化渲染]
    C2 --> E
    C3 --> E
    D1 --> E
    D2 --> E
    D3 --> E

六、WebSocket与HTTP连接对比

HTTP的自动重连机制

HTTP协议本身具备自动重连能力,主要基于以下原因:

  1. 无状态特性:HTTP是一种无状态协议,每个请求都是独立的,没有上下文依赖

    sequenceDiagram
        Client->>Server: HTTP请求
        Server->>Client: HTTP响应
        Note over Client,Server: 连接关闭
        Client->>Server: 新的HTTP请求(独立事务)
        Server->>Client: HTTP响应
    
  2. 浏览器内置重试:浏览器针对失败的HTTP请求通常有内置的重试机制

    • 网络波动导致请求失败时,浏览器会自动尝试重发
    • 用户刷新页面时会自动重新发起之前的请求
    • 浏览器实现了复杂的超时和重试策略
  3. 请求-响应模型:HTTP的请求-响应模式意味着每次交互都是全新的,不需要维护连接状态

WebSocket缺乏自动重连的原因

WebSocket协议本身不提供自动重连机制,原因如下:

  1. 有状态长连接:WebSocket是持久化的有状态连接,断开后状态丢失

    sequenceDiagram
        Client->>Server: HTTP握手请求(Upgrade: websocket)
        Server->>Client: HTTP 101切换协议
        Note over Client,Server: WebSocket连接建立
        Client->>Server: WebSocket消息
        Server->>Client: WebSocket消息
        Note over Client,Server: 连接断开(状态丢失)
    
  2. 服务端可控关闭:服务器可能出于正当理由关闭连接(资源限制、安全原因等)

    • 自动重连可能导致不必要的资源消耗
    • 可能造成意外的业务逻辑错误
  3. 重连涉及业务逻辑:重连可能需要恢复会话状态、重新认证等业务操作

    • 通用的重连策略难以满足所有场景需求

七、WebSocket与HTTP协议本质区别详解

特性HTTPWebSocket
通信模型请求-响应全双工通信
连接特性短连接(默认)长连接(持久)
协议转换不需要需要通过HTTP升级
状态管理无状态有状态
头部开销每次请求都有完整头部建立连接后头部开销小
实时能力有限(依赖轮询或SSE)原生支持实时通信
传输数据类型主要是文本支持二进制和文本
连接数限制浏览器限制并发连接数单个持久连接,节省资源
graph TD
    A[HTTP vs WebSocket] --> B[连接生命周期]
    A --> C[数据传输模式]
    A --> D[状态维护]
    
    B --> B1[HTTP: 短暂]
    B --> B2[WS: 持久]
    
    C --> C1[HTTP: 单向请求-响应]
    C --> C2[WS: 双向实时通信]
    
    D --> D1[HTTP: 无状态,依赖Cookie/Session]
    D --> D2[WS: 有状态,内置连接管理]

八、WebSocket深度拷打:协议对比与高级应用

8.1 WebSocket与其他实时通信技术对比

WebSocket并非实现实时通信的唯一选择,市场上存在多种技术方案。下面对WebSocket与长轮询、SSE、gRPC Streaming等技术进行多维度比较:

特性WebSocket长轮询 (Long Polling)SSE (Server-Sent Events)gRPC Streaming
通信方向全双工单向(客户端发起)单向(服务器推送)单向或双向流
连接特性持久TCP连接HTTP连接反复建立关闭持久HTTP连接持久HTTP/2连接
头部开销初次连接后极小每次请求完整HTTP头较小复用流的小开销
浏览器原生支持广泛支持完全支持较好支持(IE除外)需借助库
服务器实现需专门WebSocket服务器标准HTTP服务器标准HTTP服务器需gRPC支持
数据格式二进制/文本任意HTTP格式只支持文本Protocol Buffers
错误恢复协议无内置机制重新发起请求自动重连流机制支持错误处理
背后协议独立协议HTTPHTTPHTTP/2
扩展性协议级扩展有限依赖HTTP扩展有限强大RPC框架支持
graph TD
    A[实时通信技术] --> B[WebSocket]
    A --> C[长轮询]
    A --> D[SSE]
    A --> E[gRPC Streaming]
    
    B --> B1["全双工通信
自定义二进制协议
连接维护开销小"] C --> C1["兼容性好
实现简单
连接反复创建关闭"] D --> D1["单向服务器推送
HTTP标准
只支持文本"] E --> E1["基于HTTP/2
强类型
丰富RPC支持"] B1 -.-> F["高交互实时应用
游戏、协作工具"] C1 -.-> G["低频率更新
简单通知系统"] D1 -.-> H["数据流推送
实时日志、市场行情"] E1 -.-> I["系统间通信
微服务架构"]

技术机制深度解析:

  1. 长轮询 (Long Polling)

    • 客户端发起HTTP请求,服务器延迟响应直到有新数据或超时
    • 客户端收到响应后立即发起新请求,形成循环
    • 本质上是对传统轮询的优化,减少空响应带来的资源浪费
    • 依然需要频繁建立HTTP连接,每次连接都包含完整HTTP头
  2. SSE (Server-Sent Events)

    • 基于单个HTTP连接实现服务器向客户端的单向数据推送
    • 使用特殊的MIME类型text/event-stream
    • 浏览器原生支持EventSource API,自动处理重连
    • 仅支持UTF-8文本数据,不支持二进制传输
    • 默认与服务器保持长连接,服务器可持续发送消息
  3. gRPC Streaming

    • 基于HTTP/2协议的RPC框架
    • 支持单向流和双向流通信
    • 使用Protocol Buffers进行高效的二进制序列化
    • 利用HTTP/2多路复用,在单个连接上处理多个并发流
    • 提供强类型接口定义和代码生成
  4. WebSocket

    • 全双工通信协议,基于单个TCP连接
    • 通过HTTP升级机制建立,随后脱离HTTP独立运行
    • 极低的帧头开销,适合频繁小消息交换
    • 支持二进制和文本数据传输
    • 无消息队列或广播机制,需应用层实现
sequenceDiagram
    participant Client as 客户端
    participant Server as 服务器
    
    %% 长轮询
    rect rgb(200, 220, 240)
    note over Client, Server: 长轮询
    Client->>Server: HTTP 请求
    Server-->>Server: 等待数据或超时
    Server->>Client: HTTP 响应(有数据或超时)
    Client->>Server: 立即发起新HTTP请求
    end
    
    %% SSE
    rect rgb(220, 240, 200)
    note over Client, Server: SSE
    Client->>Server: HTTP 请求 (Accept: text/event-stream)
    Server->>Client: 数据流1 (text/event-stream)
    Server->>Client: 数据流2
    Server->>Client: 数据流3
    end
    
    %% WebSocket
    rect rgb(240, 220, 200)
    note over Client, Server: WebSocket
    Client->>Server: HTTP 升级请求 (Upgrade: websocket)
    Server->>Client: HTTP 101 切换协议
    Client->>Server: WebSocket 消息1
    Server->>Client: WebSocket 消息2
    Client->>Server: WebSocket 消息3
    Server->>Client: WebSocket 消息4
    end

8.2 WebSocket的优势与使用场景

WebSocket相比其他技术具有以下显著优势:

  1. 通信效率

    • 建立连接后,数据帧头部极小(2-14字节),远低于HTTP头
    • 全双工通信,无需等待请求-响应循环
    • 单个连接复用,减少握手开销
  2. 实时性能

    • 消息可即时传递,无轮询延迟
    • 直接基于TCP连接,网络层面延迟最小化
    • 服务器可主动推送,无需客户端请求
  3. 资源利用

    • 长连接减少了连接创建和销毁的CPU开销
    • 减少TCP握手导致的网络负载
    • 单连接复用降低了服务器并发连接数
  4. 功能灵活性

    • 支持二进制传输,可实现自定义协议
    • 无同源限制,更灵活的跨域通信
    • 数据格式不受限制,可传输JSON、XML、二进制等

最适合WebSocket的场景:

mindmap
  root((WebSocket
最佳场景)) 高频实时数据 股票行情 体育实况 实时监控 多人交互应用 聊天应用 协作编辑 多人游戏 需服务器推送 即时通知 报警系统 实时分析 大量并发连接 IoT设备 多终端同步 大规模监控

8.3 关键场景深度解析

8.3.1 客户端与多服务器实例连接场景

在大规模分布式系统中,客户端可能需要与多个服务器实例建立WebSocket连接。这种场景带来的挑战与解决方案:

挑战:

  • 每个WebSocket连接都消耗客户端资源(内存、网络带宽、CPU)
  • 多连接管理复杂度高,连接状态同步困难
  • 可能导致客户端设备电量消耗增加(移动设备尤为明显)

解决方案:

  1. 连接复用与分发
graph TD
    Client[客户端] --> Gateway[WebSocket网关]
    Gateway --> Server1[服务实例1]
    Gateway --> Server2[服务实例2]
    Gateway --> Server3[服务实例3]
    
    style Gateway fill:#f9f,stroke:#333,stroke-width:2px
  • 使用单一WebSocket连接到网关服务
  • 网关服务负责消息路由与分发
  • 消息头部添加路由标识符,指明目标服务
  • 实现应用层多路复用
  1. 消息代理模式
graph TD
    Client[客户端] --> Broker[消息代理]
    Broker --> Topic1[主题1]
    Broker --> Topic2[主题2]
    Topic1 --> Server1[服务实例1]
    Topic1 --> Server2[服务实例2]
    Topic2 --> Server3[服务实例3]
    
    style Broker fill:#9cf,stroke:#333,stroke-width:2px
  • 客户端连接到消息代理(如RabbitMQ、Kafka)
  • 基于主题订阅-发布模式通信
  • 降低直接连接数,提高扩展性
  1. 服务集群协同
javascript
// 客户端实现示例:按服务集群组织连接池
class ServiceConnectionPool {
  constructor() {
    this.connections = new Map(); // 服务ID -> WebSocket
    this.messageQueues = new Map(); // 服务ID -> 消息队列
    this.connectionStatus = new Map(); // 服务ID -> 连接状态
  }
  
  connect(serviceId, url) {
    if (this.connections.has(serviceId)) {
      return;
    }
    
    const ws = new WebSocket(url);
    this.connections.set(serviceId, ws);
    this.connectionStatus.set(serviceId, 'connecting');
    
    ws.onopen = () => {
      this.connectionStatus.set(serviceId, 'connected');
      this.flushQueue(serviceId);
    };
    
    ws.onclose = () => {
      this.connectionStatus.set(serviceId, 'disconnected');
      // 自动重连逻辑
    };
    
    // 处理各种事件...
  }
  
  send(serviceId, message) {
    const ws = this.connections.get(serviceId);
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    } else {
      // 连接不可用,将消息加入队列
      if (!this.messageQueues.has(serviceId)) {
        this.messageQueues.set(serviceId, []);
      }
      this.messageQueues.get(serviceId).push(message);
      
      // 尝试连接
      if (!this.connections.has(serviceId) || 
          this.connectionStatus.get(serviceId) === 'disconnected') {
        // 重新连接逻辑...
      }
    }
  }
  
  flushQueue(serviceId) {
    const queue = this.messageQueues.get(serviceId) || [];
    const ws = this.connections.get(serviceId);
    
    if (ws && ws.readyState === WebSocket.OPEN) {
      while(queue.length > 0) {
        ws.send(queue.shift());
      }
    }
  }
  
  // 其他管理方法...
}

资源优化策略:

  • 连接生命周期管理:不频繁使用的连接可主动关闭,需要时重新建立
  • 批量消息处理:合并短时间内的多个消息,减少传输次数
  • 选择性连接:仅连接当前功能所需的服务实例
  • 优先级管理:为不同服务连接设置优先级,资源受限时保证关键连接

8.3.2 服务端广播与可控消息分发

实现服务端向多个客户端分发消息的场景,关键在于消息路由和管理机制:

  1. 全局广播

服务端需要向所有已连接的客户端发送相同消息的方式:

javascript
// 服务端实现示例(Node.js + ws库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });
});

// 广播函数
function broadcast(message) {
  wss.clients.forEach(function each(client) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// 使用示例
setInterval(() => {
  broadcast(JSON.stringify({
    type: 'heartbeat',
    timestamp: Date.now()
  }));
}, 30000);
  1. 分组广播

按照特定属性将客户端分组,只向特定组的客户端广播消息:

graph TD
    Server[服务器] --> Group1[用户组A]
    Server --> Group2[用户组B]
    Server --> Group3[用户组C]
    
    Group1 --> Client1[客户端1]
    Group1 --> Client2[客户端2]
    Group2 --> Client3[客户端3]
    Group2 --> Client4[客户端4]
    Group3 --> Client5[客户端5]
    
    style Server fill:#f96,stroke:#333,stroke-width:2px
javascript
// 分组广播实现
const clientGroups = new Map(); // group -> Set<WebSocket>

function joinGroup(client, group) {
  if (!clientGroups.has(group)) {
    clientGroups.set(group, new Set());
  }
  clientGroups.get(group).add(client);
}

function leaveGroup(client, group) {
  const groupSet = clientGroups.get(group);
  if (groupSet) {
    groupSet.delete(client);
    if (groupSet.size === 0) {
      clientGroups.delete(group);
    }
  }
}

function broadcastToGroup(group, message) {
  const groupSet = clientGroups.get(group);
  if (!groupSet) return;
  
  groupSet.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

8.4 WebSocket与HTTP协议头:深入解析

8.4.1 Hop-by-hop头与End-to-end头的区别

HTTP协议头按照其传递特性可分为两类:

  1. End-to-end头(端到端头):

    • 这类头部信息必须从原始发送者传送到最终接收者(通常是客户端与源服务器)
    • 中间代理服务器必须原样转发,不能修改这些头信息
    • 即使有缓存中间层,这些头信息也必须被保留
    • 典型例子:Cache-Control, Content-Type, Content-Length, Authorization
  2. Hop-by-hop头(逐跳头):

    • 这类头部信息仅用于两个节点之间的单次传输
    • 不会被代理服务器自动转发
    • 每个传输路径上的节点可以修改或重新定义这些头
    • 典型例子:Connection, Keep-Alive, Transfer-Encoding, Upgrade
graph LR
    Client[客户端] -->|End-to-end头信息保持不变| Proxy1[代理服务器1]
    Proxy1 -->|Hop-by-hop头可能被修改或移除| Proxy2[代理服务器2]
    Proxy2 -->|End-to-end头信息仍保持不变| Server[源服务器]
    
    style Client fill:#f9f,stroke:#333,stroke-width:2px
    style Server fill:#9cf,stroke:#333,stroke-width:2px

关键点:WebSocket协议升级依赖于UpgradeConnection这两个Hop-by-hop头,这意味着:

  • 这些关键头信息可能在经过代理服务器时被修改或删除
  • 普通的反向代理可能不会默认转发这些头信息
  • 必须特别配置代理服务器以保留这些Hop-by-hop头
  • 这是为什么WebSocket连接在某些代理环境中可能无法正常工作的主要原因

8.4.2 WebSocket连接中的必要请求头配置

在WebSocket握手过程中,以下请求头是必不可少的:

  1. 必需的请求头:
请求头说明类型
Upgrade: websocket指示客户端希望升级到WebSocket协议Hop-by-hop
Connection: Upgrade告知连接需要升级Hop-by-hop
Sec-WebSocket-KeyBase64编码的16字节随机值,用于握手验证End-to-end
Sec-WebSocket-Version客户端支持的WebSocket协议版本(通常为13)End-to-end
  1. 可选但常用的请求头:
请求头说明类型
Sec-WebSocket-Protocol客户端支持的子协议列表End-to-end
Sec-WebSocket-Extensions客户端支持的协议扩展列表End-to-end
Origin发起请求的源(用于跨域安全)End-to-end

服务器响应中的必要头信息:

响应头说明类型
Upgrade: websocket确认升级到WebSocket协议Hop-by-hop
Connection: Upgrade确认连接已升级Hop-by-hop
Sec-WebSocket-Accept基于客户端的Key生成的验证码End-to-end
Sec-WebSocket-Protocol (可选)服务器选择的子协议End-to-end

8.4.3 代理环境中的WebSocket头部处理

在涉及代理服务器的环境中,WebSocket的配置尤为关键:

sequenceDiagram
    participant Client as 客户端
    participant Proxy as 代理服务器
    participant Server as WebSocket服务器
    
    Client->>+Proxy: HTTP请求
Upgrade: websocket
Connection: Upgrade Note right of Proxy: 问题:普通代理默认
不转发Hop-by-hop头 Proxy->>+Server: 修改后的HTTP请求
(可能缺少Upgrade和Connection头) Server-->>-Proxy: 400 Bad Request Proxy-->>-Client: 400 Bad Request Note over Client,Server: 正确配置代理后: Client->>+Proxy: HTTP请求
Upgrade: websocket
Connection: Upgrade Note right of Proxy: 配置代理保留
Upgrade和Connection头 Proxy->>+Server: 完整HTTP请求
Upgrade: websocket
Connection: Upgrade Server-->>-Proxy: HTTP 101 Switching Protocols Proxy-->>-Client: HTTP 101 Switching Protocols

Nginx配置示例:

nginx
# WebSocket代理正确配置
location /ws/ {
    proxy_pass http://websocket_backend;
    
    # 关键:保留Hop-by-hop头部
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
    # 传递其他End-to-end头部
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

Apache配置示例:

apache
# 启用必要的模块
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so

# WebSocket代理配置
<Location /ws/>
    ProxyPass ws://websocket_backend/ws/
    ProxyPassReverse ws://websocket_backend/ws/
</Location>

8.4.4 多层代理环境中的挑战

在现代复杂网络架构中,请求可能经过多层代理,每一层都可能对Hop-by-hop头造成影响:

常见问题及解决方案:

  1. Connection头被合并问题

    • 问题:多个代理可能会将Connection头的值合并,导致语义混乱
    • 解决方案:确保所有代理都使用HTTP/1.1并正确处理Connection头
  2. 旧代理服务器不支持WebSocket

    • 问题:某些老旧代理不识别WebSocket升级请求
    • 解决方案:通过SSL隧道(WSS)绕过代理限制,或者更新代理服务器
  3. 负载均衡与粘性会话

    • 问题:多服务器环境中,WebSocket连接可能被路由到不同服务器
    • 解决方案:配置基于IP或会话的粘性路由策略

实际案例:企业级网络环境中,由于防火墙、入侵检测系统、负载均衡器等多层代理的存在,WebSocket连接可能需要特别配置才能正常工作。一个解决方案是使用WSS(WebSocket Secure)而非WS,因为加密流量通常会被代理服务器完整传递。

javascript
// 客户端选择WSS而非WS的示例
const socket = new WebSocket('wss://example.com/socketserver');
// 而非
// const socket = new WebSocket('ws://example.com/socketserver');

总结:在涉及WebSocket的应用部署中,理解Hop-by-hop与End-to-end头部的区别至关重要。特别是在复杂的代理环境中,必须确保Upgrade和Connection这两个关键的Hop-by-hop头能够正确传递,才能成功建立WebSocket连接。

参考文章:

阮一峰:WebSocket 教程看完让你彻底理解 WebSocket 原理