WebSocket 是一种在 Web 应用程序中实现全双工通信的协议。它允许客户端和服务器之间建立一个持久的连接,双方可以在这个连接上相互发送数据,而不需要通过传统的 HTTP 请求/响应模式。这使得 WebSocket 特别适用于实时应用,如聊天应用、在线游戏、实时数据推送等。以下是 WebSocket 的工作原理的详细说明。
WebSocket 的工作原理
1. 建立连接
WebSocket 连接的建立从一个 HTTP 请求开始。客户端通过一个 HTTP 请求(通常称为 "握手" 请求)请求升级协议,从 HTTP 升级到 WebSocket。
客户端请求:
http复制代码
GET /chat HTTP/1.1 Host: 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
标头表示客户端请求升级到 WebSocket 协议。Sec-WebSocket-Key
是一个 Base64 编码的随机密钥,用于安全验证。
服务器响应:
http复制代码
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
- 状态码
101 Switching Protocols
表示服务器同意协议升级。 Sec-WebSocket-Accept
是服务器基于Sec-WebSocket-Key
计算的一个值,确保握手请求的合法性。
2. 数据传输
一旦连接建立,客户端和服务器之间的通信使用 WebSocket 帧(frame)。WebSocket 帧有不同的类型,包括文本帧、二进制帧、关闭帧、Ping 帧和 Pong 帧。
- 文本帧(Text Frame):用于传输文本数据(UTF-8 编码字符串)。
- 二进制帧(Binary Frame):用于传输二进制数据。
- 关闭帧(Close Frame):用于关闭连接。
- Ping 帧和 Pong 帧:用于连接的心跳检测,维持连接的活跃状态。
每个 WebSocket 帧都有一个固定的头部格式,用于控制帧的类型和传输的负载数据。
3. 关闭连接
WebSocket 连接可以由客户端或服务器任意一方关闭。关闭连接的过程通过发送一个关闭帧来实现,另一方收到关闭帧后会响应一个关闭帧并关闭连接。
http复制代码
// 客户端或服务器发送关闭帧 0x88 0x00
关闭帧的第一个字节 0x88
表示帧的类型为关闭帧,第二个字节 0x00
表示负载长度为 0。
WebSocket 的优点
- 全双工通信:客户端和服务器可以在同一个连接上同时发送和接收消息,减少延迟。
- 持久连接:连接一旦建立,可以持续存在,避免了 HTTP 请求/响应的开销。
- 减少带宽消耗:由于减少了 HTTP 头部的开销,WebSocket 比传统的轮询和长轮询更高效。
WebSocket 的使用
客户端代码示例(JavaScript)
javascript复制代码
// 创建 WebSocket 连接 var socket = new WebSocket("ws://example.com/socket"); // 连接打开事件 socket.onopen = function(event) { console.log("WebSocket is open now."); // 发送数据 socket.send("Hello Server!"); }; // 接收消息事件 socket.onmessage = function(event) { console.log("Received message from server:", event.data); }; // 连接关闭事件 socket.onclose = function(event) { console.log("WebSocket is closed now."); }; // 连接错误事件 socket.onerror = function(error) { console.log("WebSocket error:", error); };
服务器代码示例(Node.js + ws 库)
javascript复制代码
const WebSocket = require('ws'); // 创建 WebSocket 服务器 const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { console.log('A new client connected'); // 接收消息事件 ws.on('message', function incoming(message) { console.log('Received message:', message); // 发送消息给客户端 ws.send('Hello from server'); }); // 连接关闭事件 ws.on('close', function close() { console.log('Client disconnected'); }); });
WebSocket 协议的详细解析
WebSocket 帧格式
WebSocket 帧头部格式如下:
复制代码
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
- FIN (1 bit): 表示是否是消息的最后一帧。
- RSV1, RSV2, RSV3 (1 bit each): 保留位,通常为 0。
- Opcode (4 bits): 表示帧的类型(如文本帧、二进制帧、关闭帧等)。
- MASK (1 bit): 表示是否掩码(客户端发送给服务器的数据必须使用掩码)。
- Payload len (7 bits) : 表示负载数据的长度,如果值为
126
或127
,则使用扩展的负载长度字段。 - Extended payload length (16/64 bits): 当负载长度大于 125 时使用此字段。
- Masking-key (32 bits) : 如果
MASK
设置为 1,则此字段用于解码负载数据。 - Payload Data: 实际传输的数据。
总结
WebSocket 提供了一种在客户端和服务器之间进行实时通信的高效方法。通过一次握手请求,WebSocket 可以建立一个持久的、全双工的通信连接,减少了频繁建立和关闭连接的开销。理解其工作原理和帧格式有助于在应用中更好地使用和调试 WebSocket,从而实现高效的实时通信。