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 握手:
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
不同点:
Upgrade: WebSocket
Connection: Upgrade
你可能发现这段类似于HTTP的请求中多了Upgrade
、Connection
这两个字段。其实这就是WebSocket的核心了,告知:Apache、Nginx服务器这里是要用Websocket协议,而不是HTTP。
现在来解析一下里面的一些内容吧
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啦~
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这俩个字段依然是固定的,也就是告诉客户端即将升级WebSocket协议
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept
:经过服务器确认并且加密过后的 Sec-WebSocket-Key。Sec-WebSocket-Protocol
:最终使用协议
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包来实现的:下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。
具体的心跳检测:
- 客户端每隔一个间隔发送探测包给服务器
- 客户端发包时启动一个超时定时器
- 服务器端接收到检测包,回应一个包
- 客户端及收到服务器的心跳包,服务器正常,删除超时定时器
- 若客户端的超时定时器超时,依然没有收到应答包,则说明服务器有问题
WebSocket异常 此时需要客户端用onclose关闭连接,服务端上线前要清除之前的数据,否则只要请求服务端的都会被视为离线。 此时就需要我们处理重连,这里引入reconnecting-WebSocket.min.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
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关键配置详解:
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协议本身具备自动重连能力,主要基于以下原因:
无状态特性:HTTP是一种无状态协议,每个请求都是独立的,没有上下文依赖
sequenceDiagram Client->>Server: HTTP请求 Server->>Client: HTTP响应 Note over Client,Server: 连接关闭 Client->>Server: 新的HTTP请求(独立事务) Server->>Client: HTTP响应
浏览器内置重试:浏览器针对失败的HTTP请求通常有内置的重试机制
- 网络波动导致请求失败时,浏览器会自动尝试重发
- 用户刷新页面时会自动重新发起之前的请求
- 浏览器实现了复杂的超时和重试策略
请求-响应模型:HTTP的请求-响应模式意味着每次交互都是全新的,不需要维护连接状态
WebSocket缺乏自动重连的原因
WebSocket协议本身不提供自动重连机制,原因如下:
有状态长连接: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: 连接断开(状态丢失)
服务端可控关闭:服务器可能出于正当理由关闭连接(资源限制、安全原因等)
- 自动重连可能导致不必要的资源消耗
- 可能造成意外的业务逻辑错误
重连涉及业务逻辑:重连可能需要恢复会话状态、重新认证等业务操作
- 通用的重连策略难以满足所有场景需求
七、WebSocket与HTTP协议本质区别详解
特性 | HTTP | WebSocket |
---|---|---|
通信模型 | 请求-响应 | 全双工通信 |
连接特性 | 短连接(默认) | 长连接(持久) |
协议转换 | 不需要 | 需要通过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 |
错误恢复 | 协议无内置机制 | 重新发起请求 | 自动重连 | 流机制支持错误处理 |
背后协议 | 独立协议 | HTTP | HTTP | HTTP/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["系统间通信
微服务架构"]
技术机制深度解析:
长轮询 (Long Polling)
- 客户端发起HTTP请求,服务器延迟响应直到有新数据或超时
- 客户端收到响应后立即发起新请求,形成循环
- 本质上是对传统轮询的优化,减少空响应带来的资源浪费
- 依然需要频繁建立HTTP连接,每次连接都包含完整HTTP头
SSE (Server-Sent Events)
- 基于单个HTTP连接实现服务器向客户端的单向数据推送
- 使用特殊的MIME类型
text/event-stream
- 浏览器原生支持EventSource API,自动处理重连
- 仅支持UTF-8文本数据,不支持二进制传输
- 默认与服务器保持长连接,服务器可持续发送消息
gRPC Streaming
- 基于HTTP/2协议的RPC框架
- 支持单向流和双向流通信
- 使用Protocol Buffers进行高效的二进制序列化
- 利用HTTP/2多路复用,在单个连接上处理多个并发流
- 提供强类型接口定义和代码生成
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相比其他技术具有以下显著优势:
通信效率
- 建立连接后,数据帧头部极小(2-14字节),远低于HTTP头
- 全双工通信,无需等待请求-响应循环
- 单个连接复用,减少握手开销
实时性能
- 消息可即时传递,无轮询延迟
- 直接基于TCP连接,网络层面延迟最小化
- 服务器可主动推送,无需客户端请求
资源利用
- 长连接减少了连接创建和销毁的CPU开销
- 减少TCP握手导致的网络负载
- 单连接复用降低了服务器并发连接数
功能灵活性
- 支持二进制传输,可实现自定义协议
- 无同源限制,更灵活的跨域通信
- 数据格式不受限制,可传输JSON、XML、二进制等
最适合WebSocket的场景:
mindmap root((WebSocket
最佳场景)) 高频实时数据 股票行情 体育实况 实时监控 多人交互应用 聊天应用 协作编辑 多人游戏 需服务器推送 即时通知 报警系统 实时分析 大量并发连接 IoT设备 多终端同步 大规模监控
8.3 关键场景深度解析
8.3.1 客户端与多服务器实例连接场景
在大规模分布式系统中,客户端可能需要与多个服务器实例建立WebSocket连接。这种场景带来的挑战与解决方案:
挑战:
- 每个WebSocket连接都消耗客户端资源(内存、网络带宽、CPU)
- 多连接管理复杂度高,连接状态同步困难
- 可能导致客户端设备电量消耗增加(移动设备尤为明显)
解决方案:
- 连接复用与分发
graph TD Client[客户端] --> Gateway[WebSocket网关] Gateway --> Server1[服务实例1] Gateway --> Server2[服务实例2] Gateway --> Server3[服务实例3] style Gateway fill:#f9f,stroke:#333,stroke-width:2px
- 使用单一WebSocket连接到网关服务
- 网关服务负责消息路由与分发
- 消息头部添加路由标识符,指明目标服务
- 实现应用层多路复用
- 消息代理模式
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)
- 基于主题订阅-发布模式通信
- 降低直接连接数,提高扩展性
- 服务集群协同
// 客户端实现示例:按服务集群组织连接池
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 服务端广播与可控消息分发
实现服务端向多个客户端分发消息的场景,关键在于消息路由和管理机制:
- 全局广播
服务端需要向所有已连接的客户端发送相同消息的方式:
// 服务端实现示例(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);
- 分组广播
按照特定属性将客户端分组,只向特定组的客户端广播消息:
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
// 分组广播实现
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协议头按照其传递特性可分为两类:
End-to-end头(端到端头):
- 这类头部信息必须从原始发送者传送到最终接收者(通常是客户端与源服务器)
- 中间代理服务器必须原样转发,不能修改这些头信息
- 即使有缓存中间层,这些头信息也必须被保留
- 典型例子:Cache-Control, Content-Type, Content-Length, Authorization
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协议升级依赖于Upgrade和Connection这两个Hop-by-hop头,这意味着:
- 这些关键头信息可能在经过代理服务器时被修改或删除
- 普通的反向代理可能不会默认转发这些头信息
- 必须特别配置代理服务器以保留这些Hop-by-hop头
- 这是为什么WebSocket连接在某些代理环境中可能无法正常工作的主要原因
8.4.2 WebSocket连接中的必要请求头配置
在WebSocket握手过程中,以下请求头是必不可少的:
- 必需的请求头:
请求头 | 说明 | 类型 |
---|---|---|
Upgrade: websocket | 指示客户端希望升级到WebSocket协议 | Hop-by-hop |
Connection: Upgrade | 告知连接需要升级 | Hop-by-hop |
Sec-WebSocket-Key | Base64编码的16字节随机值,用于握手验证 | End-to-end |
Sec-WebSocket-Version | 客户端支持的WebSocket协议版本(通常为13) | End-to-end |
- 可选但常用的请求头:
请求头 | 说明 | 类型 |
---|---|---|
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配置示例:
# 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配置示例:
# 启用必要的模块
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头造成影响:
常见问题及解决方案:
Connection头被合并问题
- 问题:多个代理可能会将Connection头的值合并,导致语义混乱
- 解决方案:确保所有代理都使用HTTP/1.1并正确处理Connection头
旧代理服务器不支持WebSocket
- 问题:某些老旧代理不识别WebSocket升级请求
- 解决方案:通过SSL隧道(WSS)绕过代理限制,或者更新代理服务器
负载均衡与粘性会话
- 问题:多服务器环境中,WebSocket连接可能被路由到不同服务器
- 解决方案:配置基于IP或会话的粘性路由策略
实际案例:企业级网络环境中,由于防火墙、入侵检测系统、负载均衡器等多层代理的存在,WebSocket连接可能需要特别配置才能正常工作。一个解决方案是使用WSS(WebSocket Secure)而非WS,因为加密流量通常会被代理服务器完整传递。
// 客户端选择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连接。
参考文章: