随着互联网技术的飞速发展,实时通信应用越来越受到用户的青睐。实时通信已经成为许多应用的核心需求。无论是在线聊天、实时协作还是游戏,低延迟和高效的通信机制都是不可或缺的。
传统的 HTTP 协议由于其无状态和请求-响应模型,难以满足这些实时应用的需求。这时,WebSocket 协议应运而生,它提供了一种全双工的通信通道,使得客户端和服务器之间可以进行实时、双向的数据交换。
协议握手过程
WebSocket协议的握手过程是建立连接的第一步,它基于HTTP协议进行,目的是将普通的HTTP连接升级为WebSocket连接。下面是握手过程的基本步骤:
客户端发起连接请求:
客户端通过发送一个HTTP的GET请求到服务器指定的WebSocket URL(通常是ws://或wss://开头)。这个请求包含了特殊的头信息,表明这是一个WebSocket连接请求。其中关键的头信息包括但不限于:
Upgrade: 必须设置为websocket,表明客户端希望将连接升级为WebSocket。Connection: 必须设置为Upgrade,同样指示了升级连接的意图。Sec-WebSocket-Key: 包含一个随机生成的Base64编码的密钥。服务器将使用这个密钥加上一个固定的字符串,然后进行SHA-1哈希,并进行Base64编码,作为响应的一部分返回,以此来验证客户端。Sec-WebSocket-Version: 指定WebSocket协议版本,确保客户端和服务器支持相同的协议版本。
服务器响应:
服务器收到请求后,如果同意升级,则返回一个HTTP响应,状态码为101 Switching Protocols,表示服务器同意切换协议。响应中同样包含特定的头信息:
Upgrade: 响应中也必须包含此字段,并设置为websocket。Connection: 同样设置为Upgrade,确认连接升级。Sec-WebSocket-Accept: 服务器根据客户端发送的Sec-WebSocket-Key,加上一个固定的GUID字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11),进行SHA-1哈希后进行Base64编码得到的值。这是服务器对客户端握手请求的确认。
握手完成:
当客户端收到服务器的正确响应后,握手过程完成,此时HTTP连接就被转换成了WebSocket连接。接下来,双方就可以开始发送和接收WebSocket数据帧,进行全双工的实时通信。
客户端(JavaScript):
在客户端,我们通常使用WebSocket API来建立连接。以下是一个简单的WebSocket连接初始化代码:
var socket = new WebSocket("ws://example.com/socketserver");
socket.onopen = function(event) {
console.log("Connection open!");
socket.send("Hello Server!");
};
socket.onmessage = function(event) {
console.log("Message from server:", event.data);
};
socket.onerror = function(error) {
console.error("Error detected: " + error);
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
console.log('[close] Connection died');
}
};
这段代码中,new WebSocket(...)就是发起WebSocket连接请求的地方,握手过程自动由浏览器处理,无需手动编写HTTP请求头。onopen、onmessage、onerror、onclose是WebSocket对象的事件处理器,分别对应连接成功、接收到消息、发生错误、连接关闭的情况。
服务器端(Node.js + ws库):
在服务器端,可以使用各种库来处理WebSocket连接,这里以Node.js的ws库为例:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log(`Received message => ${message}`);
ws.send(`Hello! You sent -> ${message}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
ws.on('error', (err) => {
console.error('WebSocket error observed:', err);
});
});
这段代码创建了一个监听8080端口的WebSocket服务器。当有客户端连接时,connection事件会被触发,服务器可以开始接收和发送消息。在这个例子中,服务器简单地将接收到的消息原样回复给客户端。
数据帧格式解析
WebSocket的数据传输基于一种称为数据帧(frame)的结构化格式,它允许在连接上高效、可靠地传输数据。每个数据帧都包含一个固定格式的头部和可选的负载数据。下面简要介绍数据帧的结构:
数据帧结构:
- Fin: 1位,表示是否是消息的最后一个帧。如果是单帧消息,该位为1。
- RSV1, RSV2, RSV3: 各1位,通常用于扩展定义,如协议协商时的掩码位或压缩标识。默认情况下应为0。
- Opcode: 4位,定义了帧的类型。常见的有:
- 0x0:表示延续帧,用于多帧消息的后续部分。
- 0x1:表示文本帧。
- 0x2:表示二进制帧。
- 0x8:表示连接关闭。
- 0x9:表示ping。
- 0xA:表示pong。
- Mask: 1位,客户端发往服务器的数据帧必须设置为1,并附带掩码密钥。
- Payload length: 可变长度字段,表示负载数据的长度。根据长度的不同,有不同的编码方式。
- Masking-key: 如果Mask位为1,则存在这四个字节的掩码密钥,用于解码负载数据。
- Payload data: 负载数据,可以是文本或二进制数据。
一个简单的WebSocket服务器,需要解析接收到的数据帧,可以遵循以下步骤:
-
读取第一个字节:这个字节包含了Fin、RSV*以及Opcode的信息。你可以通过位操作(如位与、位移)来提取这些信息。
-
检查Mask位:第二个字节的最高位表示Mask。如果为1,表示接下来有4个字节的掩码密钥。
-
确定Payload length:接下来的字节或字节序列表示负载长度。长度小于126时,直接使用该字节的值;长度为126时,接下来的两个字节表示长度(16位);长度为127时,接下来的8个字节表示长度(64位,一般很少使用)。
-
处理Masking-key:如果有掩码,读取接下来的4个字节作为掩码密钥。
-
解码Payload data:根据Payload length读取相应长度的数据。如果存在掩码,使用掩码密钥对数据进行解码。解码方法通常是按字节异或掩码密钥。
-
处理Fin和Opcode:根据Fin位判断是否还有更多帧需要处理;根据Opcode决定如何处理当前帧的数据,如文本帧需要解码为字符串,二进制帧则直接处理。
在JavaScript中,如果你使用的是WebSocket API,通常不需要直接处理数据帧的解析,因为WebSocket API已经为我们抽象了这些底层细节。然而,为了提供一个概念性的理解,我们可以模拟一个简化的数据帧解析过程,假设你已经收到了WebSocket帧的原始字节数据(这在实践中很少直接发生,但有助于理解内部机制)。
function parseWebSocketFrame(frameData) {
let byteIndex = 0;
// 解析Fin和Opcode
const fin = (frameData[byteIndex] & 0b10000000) !== 0;
const opcode = frameData[byteIndex++] & 0b00001111;
// 解析Mask和Payload Length
const mask = (frameData[byteIndex] & 0b10000000) !== 0;
let payloadLength = frameData[byteIndex++] & 0b01111111;
// 根据Payload Length的值确定实际长度
if (payloadLength === 126) {
payloadLength = (frameData[byteIndex++] << 8) | frameData[byteIndex++];
} else if (payloadLength === 127) {
// 这里简化处理,实际应用中需要处理64位长度
throw new Error("Handling of 127-length not implemented.");
}
// 获取Masking Key(如果有)
let maskingKey;
if (mask) {
maskingKey = new Uint8Array([frameData[byteIndex++], frameData[byteIndex++], frameData[byteIndex++], frameData[byteIndex++]]);
}
// 提取Payload Data
const payloadStart = byteIndex;
const payloadEnd = payloadStart + payloadLength;
const payloadData = new Uint8Array(frameData.slice(payloadStart, payloadEnd));
// 如果有Mask,解码Payload
if (mask) {
for (let i = 0; i < payloadLength; i++) {
payloadData[i] ^= maskingKey[i % 4];
}
}
// 根据Opcode处理Payload
switch (opcode) {
case 0x1: // 文本帧
return { type: 'text', data: new TextDecoder().decode(payloadData) };
case 0x2: // 二进制帧
return { type: 'binary', data: payloadData };
// ...其他类型的处理
default:
throw new Error(`Unsupported opcode: ${opcode.toString(16)}.`);
}
}
// 示例使用(注意:在实际WebSocket连接中,你不会直接获得这样的frameData)
// let frameData = ...; // 假设这是从WebSocket连接中获取的原始帧数据
// try {
// let parsedFrame = parseWebSocketFrame(frameData);
// console.log(parsedFrame);
// } catch (e) {
// console.error(e);
// }
连接建立与关闭流程
WebSocket协议的连接建立与关闭流程是其生命周期中的两个关键步骤,它们确保了客户端与服务器之间的稳定、可靠的双向通信。
连接建立流程(Handshaking)
客户端发起请求:
客户端通过HTTP的Upgrade请求向服务器发起连接,请求头中包含特殊的字段来指示这是一个WebSocket连接请求。典型的请求头包括:
Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: 一个Base64编码的随机字符串,用于握手验证。Sec-WebSocket-Version: 表明客户端支持的WebSocket协议版本。
服务器响应:
服务器收到请求后,如果接受连接,会返回一个HTTP响应,状态码为101 Switching Protocols,表示协议升级成功。响应头中包含:
Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: 服务器根据客户端发送的Sec-WebSocket-Key计算出的值,用作握手验证。
握手完成:
客户端收到正确的响应后,握手完成,WebSocket连接正式建立,双方可以开始发送数据帧进行通信。
数据传输
握手成功后,WebSocket连接进入数据传输阶段,双方可以通过发送数据帧来进行数据交换。数据帧包含一个头部用于描述帧类型、长度等信息,以及可选的有效载荷数据。
连接关闭流程
- 关闭发起:
任何一端都可以发起关闭连接的请求。关闭请求通常包含一个关闭状态码(Close Code)和一个可选的原因说明(Close Reason),这些信息被封装在关闭帧中发送。
- 关闭响应:
接收方收到关闭请求后,会发送一个确认关闭的帧回给发起方,这个帧也可以包含状态码和原因说明。
- 四次挥手(TCP层面):
WebSocket是建立在TCP之上的,因此实际的连接关闭会经历TCP的四次挥手过程,确保数据的完整传输和资源的正确释放。
- 连接关闭:
双方都发送并接收到了关闭帧后,连接正式关闭。客户端和服务器端都会触发相应的关闭事件(如onclose事件),应用程序可以在此时进行清理工作。
关闭状态码(Close Codes)
- 1000: 正常关闭,表示连接被成功关闭且没有异常。
- 1001: 终端离开,比如页面关闭。
- 1002: 协议错误,接收到不符合协议格式的数据。
- 1003: 不可接受的数据类型,接收到的数据类型不受支持。
- 1005: 未定义,没有状态码(通常是因为关闭帧没有携带状态码)。
- 1006: 未定义,连接意外关闭(比如网络中断)。
- 1007: 数据帧中的数据违反了约定的子协议。
- 1008: 接收到无效的数据。
- 1009: 数据帧过大,无法处理。
- 1010: 客户端期望的服务端没有提供的扩展。
- 1011: 由于服务器端错误导致的连接关闭。
- 1015: TLS握手失败。
理解WebSocket的连接建立与关闭流程对于开发实时通信应用至关重要,它确保了通信的高效、有序和安全。