http 基于 websocket 协议通信
1. 基础概念
1.1. WebSocket 和 TCP/IP 模型
WebSocket 是一种在 OSI 模型中位于应用层的协议,而 TCP/IP 模型中分为四层:网络接口层、网络层、传输层、应用层。TCP/IP 模型中 WebSocket 和 HTTP 都工作在应用层,但两者的交互模型不同:
HTTP:请求-响应模式,每次通信都需要重新建立连接。
WebSocket:在 HTTP 握手后,使用单个 TCP 连接进行全双工的通信,服务器和客户端可以随时互相发送消息。
1.2. WebSocket 的工作原理
HTTP 握手:WebSocket 建立时首先会通过 HTTP 完成一次握手请求,客户端向服务器发送包含 Upgrade 头的 HTTP 请求,表明希望升级连接到 WebSocket 协议。
协议升级:服务器检查请求头,确认可以升级,并返回 101 状态码,表示协议切换成功。此时,HTTP 请求转变为 WebSocket 双向通信。
数据帧传输:建立 WebSocket 连接后,通信不再是 HTTP 报文,而是使用一种特殊的二进制数据帧格式进行通信。WebSocket 使用较轻的帧协议,客户端和服务器可以自由地在该连接上发送和接收消息。
1.3. WebSocket 握手详解
1.3.1. 请求(客户端发起的握手请求)
客户端发送一个 HTTP 请求,带有特殊的 WebSocket 头部。一个典型的握手请求如下:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Upgrade: websocket:告诉服务器希望升级协议到 WebSocket。
Connection: Upgrade:标记为升级连接。
Sec-WebSocket-Key:Base64 编码的随机值,由服务器用于计算响应的 Sec-WebSocket-Accept。
Sec-WebSocket-Version:指定 WebSocket 协议版本。
1.3.2. 响应(服务器端的握手响应)
服务器验证了请求并准备升级协议时,会返回类似以下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols:HTTP 状态码,表明协议正在切换。
Sec-WebSocket-Accept:根据客户端的 Sec-WebSocket-Key 计算得出的 Base64 编码字符串,表明握手成功。
2. 实现 WebSocket 握手和通信
接下来,我们用 http 模块实现基本的 WebSocket 协议。我们将通过 HTTP 完成握手,之后转入 WebSocket 的二进制通信。
2.1. 创建 HTTP 服务器并实现 WebSocket 握手
const http = require('http');
const crypto = require('crypto');// 创建HTTP服务器
const server = http.createServer((req, res) => {// 不处理常规HTTP请求res.writeHead(400);res.end('This is a WebSocket server!');
});// 监听 upgrade 事件,处理 WebSocket 协议升级
server.on('upgrade', (req, socket, head) => {const key = req.headers['sec-websocket-key'];const acceptKey = generateAcceptValue(key);// 构造 WebSocket 握手响应const responseHeaders = ['HTTP/1.1 101 Switching Protocols','Upgrade: websocket','Connection: Upgrade',`Sec-WebSocket-Accept: ${acceptKey}`];// 将响应头写入,并升级连接socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');// 接下来可以监听和发送 WebSocket 帧socket.on('data', buffer => {// 处理 WebSocket 数据帧(后续将详细说明)console.log('Received data:', buffer);});// 发送 WebSocket 帧const message = 'Hello WebSocket';const frame = createWebSocketFrame(message);socket.write(frame);
});// 生成 Sec-WebSocket-Accept 值
function generateAcceptValue(secWebSocketKey) {return crypto.createHash('sha1').update(secWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
}// WebSocket 帧创建(文本帧)
function createWebSocketFrame(data) {const message = Buffer.from(data);const frame = Buffer.alloc(message.length + 2);// 0x81 表示文本帧(FIN + 1 表示文本帧)frame[0] = 0x81;frame[1] = message.length;message.copy(frame, 2);return frame;
}server.listen(8080, () => {console.log('WebSocket server running on port 8080');
});
2.2. 握手解析
当客户端发起连接时,服务器通过监听 upgrade 事件响应协议升级请求。
在协议升级时,服务器使用 Sec-WebSocket-Key 和 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 这串固定字符串生成一个 Base64 编码的 SHA1 哈希,作为 Sec-WebSocket-Accept 头的值。
握手成功后,服务器将升级连接,并准备处理 WebSocket 数据帧。
2.3. WebSocket 数据帧格式
WebSocket 的数据通信是通过帧来进行的,每一帧都包含了如下字段:
FIN:标志帧是否是消息的最后一帧。
Opcode:定义了数据的类型,如文本帧、二进制帧等。
Mask:掩码标志,客户端发送给服务器的数据必须进行掩码处理。
Payload:实际传输的数据。
2.4. 处理 WebSocket 帧
在服务器接收到数据时,需要解码 WebSocket 帧。根据协议规范,我们解析接收到的二进制帧。
// 解析 WebSocket 帧
function parseWebSocketFrame(buffer) {const fin = buffer[0] & 0x80; // FIN bitconst opcode = buffer[0] & 0x0f; // Opcodeconst masked = buffer[1] & 0x80; // Mask bitlet payloadLength = buffer[1] & 0x7f; // Payload Lengthlet offset = 2;if (payloadLength === 126) {payloadLength = buffer.readUInt16BE(2);offset += 2;} else if (payloadLength === 127) {payloadLength = buffer.readUInt32BE(2); // For Larger payloadoffset += 8;}const mask = masked ? buffer.slice(offset, offset + 4) : null;offset += masked ? 4 : 0;let payload = buffer.slice(offset, offset + payloadLength);// 如果有掩码,需要解码 payloadif (masked) {payload = payload.map((byte, i) => byte ^ mask[i % 4]);}return payload.toString();
}
在服务器接收到客户端的帧后,我们解析出消息内容,并根据需求进行处理。
通过 http 模块直接实现 WebSocket 服务器,涉及到了:
WebSocket 握手机制:使用 HTTP 协议完成初始的握手,并通过升级协议从 HTTP 转为 WebSocket。
TCP 长连接:WebSocket 在 HTTP 握手后,建立在单个 TCP 连接上,实现全双工通信。
WebSocket 数据帧协议:通过帧的结构定义消息传输的格式,服务器需解析和处理这些帧。
3. socket.io
socket.io 是一个广泛使用的 JavaScript 库,专门用于实现实时、双向的通信。与 WebSocket 类似,socket.io 建立在 TCP 之上,通过使用 WebSocket 和其他协议(如长轮询)来确保实时通信的顺利进行。socket.io 不仅简化了 WebSocket 的实现,而且提供了额外的功能,如自动重连、命名空间、事件广播等,因此它成为许多实时应用的首选。
WebSocket:一种在客户端和服务器之间建立全双工通信的协议,适合实时数据传输。socket.io 使用 WebSocket 来进行通信,但在无法使用 WebSocket 的环境中,它会降级到其他协议,如 HTTP 长轮询。
命名空间:socket.io 可以通过命名空间来分离不同的通信通道,这样不同的业务逻辑可以运行在不同的命名空间中,避免混乱。
房间:socket.io 可以将不同的连接分组到房间中,一个房间中的所有客户端可以相互通信,这在实现聊天应用时非常有用。
3.1. socket.io 的优势
跨协议支持:如果 WebSocket 不可用,socket.io 可以回退到轮询等其他机制,确保连接的稳定性。
事件驱动:socket.io 使用事件驱动的方式通信,可以方便地绑定各种事件,像 message、connect、disconnect 等。
自动重连:当连接丢失时,socket.io 提供自动重连机制,确保客户端和服务器保持连接。
3.2. 服务端与客户端基本实现
3.2.1. 安装 socket.io
首先,我们需要安装 socket.io 和 socket.io-client 两个库,分别用于服务端和客户端。
npm install socket.io
npm install socket.io-client
3.2.2. 创建 socket.io 服务端
在服务端,我们使用 http 模块创建一个 HTTP 服务器,然后在其基础上初始化一个 socket.io 实例。
const http = require('http');
const { Server } = require('socket.io');// 创建HTTP服务器
const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'text/plain' });res.end('socket.io Server');
});// 初始化socket.io服务器
const io = new Server(server);io.on('connection', (socket) => {console.log(`A user connected [ID: ${socket.id}]`);// 接收客户端消息socket.on('message', (data) => {console.log(`Received message: ${data}`);socket.emit('response', `Server received your message: ${data}`);});// 监听断开连接socket.on('disconnect', () => {console.log('User disconnected');});
});// 启动服务器
server.listen(3000, () => {console.log('socket.io server is running on port 3000');
});
3.2.3. 创建 socket.io 客户端
客户端可以使用 socket.io-client 库与服务器建立实时连接。以下是一个在浏览器端实现的例子:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>socket.io Client</title><script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body><h1>socket.io Client</h1><input id="messageInput" type="text" placeholder="Type your message"><button id="sendButton">Send</button><div id="response"></div><script>// 创建socket.io客户端const socket = io('http://localhost:3000');// 发送消息const sendButton = document.getElementById('sendButton');const messageInput = document.getElementById('messageInput');sendButton.addEventListener('click', () => {const message = messageInput.value;socket.emit('message', message);});// 接收服务器的响应socket.on('response', (data) => {document.getElementById('response').textContent = data;});</script>
</body>
</html>
3.2.4. 命名空间和房间
1. 使用命名空间
命名空间允许我们将一个 socket.io 实例划分为多个逻辑空间。客户端可以选择连接到不同的命名空间。
// 服务端:命名空间
const chatNamespace = io.of('/chat');chatNamespace.on('connection', (socket) => {console.log('User connected to /chat');socket.on('message', (data) => {chatNamespace.emit('message', data);});
});// 客户端:连接到指定命名空间
const socket = io('http://localhost:3000/chat');
2. 使用房间
房间允许我们将连接分组,可以实现针对特定房间的消息广播。
io.on('connection', (socket) => {// 加入房间socket.join('room1');// 向房间内的所有客户端发送消息socket.to('room1').emit('message', 'A new user has joined room1');socket.on('message', (data) => {io.to('room1').emit('message', data);});
});
3.2.5. 深入探讨
1. socket.io 与 WebSocket 的区别
协议回退:WebSocket 仅支持 WebSocket 协议,而 socket.io 可以在 WebSocket 不可用时回退到 HTTP 长轮询等其他协议,保证更高的连接可靠性。
消息格式:WebSocket 使用帧进行消息传输,socket.io 则在 WebSocket 之上增加了更多的控制帧,使其更适合构建复杂的应用。
事件机制:socket.io 提供了事件驱动的通信模型,便于开发者处理各种业务逻辑。而 WebSocket 是基于较底层的 API。
2. 使用场景
socket.io 非常适合需要实时通信的应用场景,例如:
实时聊天应用:用户可以在多个房间中发送消息,服务器可以立即将消息广播给其他用户。
在线游戏:服务器可以实时同步游戏状态,保证玩家间的互动体验。
实时数据推送:例如股票市场更新、体育赛事直播等需要推送最新信息的应用场景。