各位同学,今天咱们来聊聊 WebSocket 这个神奇的东西,保证让大家听完之后,感觉自己都能徒手撸一个 WebSocket 服务器出来!
一、WebSocket 帧结构:数据传输的骨骼
想象一下,你要给朋友寄一箱好吃的,总不能直接把吃的扔过去吧?得先装到箱子里,贴上标签,确保对方知道这是啥,怎么打开。WebSocket 帧就是这个“箱子”。
WebSocket 协议基于帧进行数据传输。每一个帧都包含了控制信息和实际的数据。下面咱们来解剖一下这个“箱子”:
字段名称 | 长度 (bit) | 描述 |
---|---|---|
FIN | 1 | 表示这是消息的最后一帧,如果为 1,则表示这是消息的最后一部分或者完整的消息。 |
RSV1, RSV2, RSV3 | 1 | 保留位,通常为 0。可以用来扩展协议。 |
Opcode | 4 | 操作码,定义了帧数据的类型。例如:文本数据、二进制数据、Ping、Pong、关闭连接等。 |
Mask | 1 | 指示 Payload Data 是否进行了掩码处理。如果是从客户端发送到服务器的数据,Mask 必须为 1。 |
Payload Length | 7, 7+16, 7+64 | Payload Data 的长度。如果 Payload Length 为 0-125,则表示 Payload Data 的实际长度。如果为 126,则后面的 2 个字节表示 Payload Data 的长度。如果为 127,则后面的 8 个字节表示 Payload Data 的长度。 |
Masking-key | 32 | 掩码密钥,用于对 Payload Data 进行掩码处理。只有当 Mask 为 1 时才存在。 |
Payload Data | 变长 | 实际的数据。如果进行了掩码处理,则需要使用 Masking-key 进行解掩码。 |
- FIN(最后的舞步): 告诉接收方:“嘿,这是我最后一次发货啦!可以打包收工了!”
- RSV1, RSV2, RSV3(预留座位): 暂时没啥用,留给以后升级用的。
-
Opcode(暗号): 告诉接收方:“我发的是文字、图片、还是心跳包?” 几个常见的 Opcode 值:
0x00
: Continuation Frame (继续帧,用于分片传输)0x01
: Text Frame (文本帧)0x02
: Binary Frame (二进制帧)0x08
: Connection Close Frame (关闭连接帧)0x09
: Ping Frame (Ping 帧)0x0A
: Pong Frame (Pong 帧)
- Mask(面具): 客户端发送给服务器的数据必须戴上面具(进行掩码处理),防止一些恶意攻击。服务器发给客户端的数据不用戴面具。
- Payload Length(包裹大小): 告诉接收方:“我这个箱子有多大!”
- Masking-key(解密钥匙): 如果戴了面具,就需要这把钥匙来解开数据。
- Payload Data(包裹内容): 真正的数据!
举个栗子:
假设客户端要发送 "Hello" 这个文本消息给服务器。
- FIN: 1 (这是最后也是唯一的一帧)
- RSV1, RSV2, RSV3: 0, 0, 0
- Opcode: 0x01 (文本帧)
- Mask: 1 (客户端必须掩码)
- Payload Length: 5 (因为 "Hello" 有 5 个字符)
- Masking-key: 假设是
0x12345678
(4 个字节) - Payload Data: "Hello" 经过掩码处理后的数据。 掩码的算法很简单:把 Payload Data 的每个字节和 Masking-key 循环异或一下。
二、Ping/Pong 机制:心跳的节奏
现在你和你的朋友都收到包裹了,但是怎么知道对方还活着,还在接收你的包裹呢?这就是 Ping/Pong 机制的作用!
WebSocket 协议提供了 Ping 和 Pong 帧,用来检测连接的有效性。
- Ping: 一方发送 Ping 帧给另一方,就像问:“嘿,你还在吗?”
- Pong: 另一方收到 Ping 帧后,必须立即回复一个 Pong 帧,就像回答:“我还在!我很好!”
Ping/Pong 帧的 Opcode 分别是 0x09
和 0x0A
。 Pong 帧的 Payload Data 必须和 Ping 帧的 Payload Data 完全一样。
代码示例 (Python):
import asyncio
import websockets
async def ping_pong_handler(websocket, path):
try:
while True:
# 接收消息
message = await websocket.recv()
print(f"Received message: {message}")
# 如果收到 Ping,回复 Pong
if message == "ping": # 简化示例,实际应该解析 Opcode
await websocket.send("pong")
print("Sent pong")
except websockets.exceptions.ConnectionClosedOK:
print("Client disconnected normally")
except websockets.exceptions.ConnectionClosedError as e:
print(f"Client disconnected with error: {e}")
except Exception as e:
print(f"An error occurred: {e}")
async def main():
async with websockets.serve(ping_pong_handler, "localhost", 8765):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
三、心跳检测:保持连接的秘诀
心跳检测就是利用 Ping/Pong 机制,定期检查连接是否还活着。 如果一段时间内没有收到 Pong 帧,就认为连接已经断开,需要进行重连。
心跳检测的流程:
- 定时发送 Ping 帧: 客户端或服务器(通常是客户端)每隔一段时间(例如 30 秒)发送一个 Ping 帧。
- 等待 Pong 帧: 发送 Ping 帧后,等待对方回复 Pong 帧。
- 超时判断: 如果在一定时间内(例如 10 秒)没有收到 Pong 帧,就认为连接已经断开。
- 重连: 如果连接断开,就尝试重新建立连接。
代码示例 (JavaScript 客户端):
let websocket;
let pingInterval;
let timeout = 5000 // 超时时间 5s
function connect() {
websocket = new WebSocket("ws://localhost:8765");
websocket.onopen = () => {
console.log("Connected to WebSocket server");
startHeartbeat();
};
websocket.onmessage = (event) => {
console.log("Received message:", event.data);
if (event.data === "pong") {
resetTimeout(); // 收到pong重置超时计时器
}
};
websocket.onclose = () => {
console.log("Disconnected from WebSocket server");
clearInterval(pingInterval);
setTimeout(connect, 3000); // 3 秒后尝试重连
};
websocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
function startHeartbeat() {
pingInterval = setInterval(() => {
sendPing();
startTimeout();
}, 30000); // 每 30 秒发送一次 Ping
}
function sendPing() {
if (websocket.readyState === WebSocket.OPEN) {
websocket.send("ping");
console.log("Sent ping");
}
}
let timeoutId
function startTimeout() {
timeoutId = setTimeout(() => {
console.log('Timeout, closing connection');
websocket.close();
}, timeout);
}
function resetTimeout() {
clearTimeout(timeoutId);
startTimeout();
}
connect(); // 启动连接
四、断线重连:生命不息,重连不止
断线重连是心跳检测的最终目的。 当检测到连接断开时,客户端需要自动尝试重新建立连接,以保证服务的可用性。
断线重连的策略:
- 立即重连: 立即尝试重新连接。
- 延迟重连: 等待一段时间后再尝试重连,避免频繁重连导致服务器压力过大。
- 指数退避: 每次重连失败后,延迟的时间呈指数增长,例如第一次延迟 1 秒,第二次延迟 2 秒,第三次延迟 4 秒,以此类推。 这样做可以避免在服务器出现问题时,客户端一直疯狂重连。
代码示例 (JavaScript 客户端,包含指数退避):
let websocket;
let pingInterval;
let reconnectDelay = 1000; // 初始重连延迟
const maxReconnectDelay = 30000; // 最大重连延迟
function connect() {
websocket = new WebSocket("ws://localhost:8765");
websocket.onopen = () => {
console.log("Connected to WebSocket server");
startHeartbeat();
reconnectDelay = 1000; // 重置重连延迟
};
websocket.onmessage = (event) => {
console.log("Received message:", event.data);
};
websocket.onclose = () => {
console.log("Disconnected from WebSocket server");
clearInterval(pingInterval);
reconnect(); // 调用重连函数
};
websocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
function startHeartbeat() {
pingInterval = setInterval(() => {
sendPing();
}, 30000); // 每 30 秒发送一次 Ping
}
function sendPing() {
if (websocket.readyState === WebSocket.OPEN) {
websocket.send("ping");
console.log("Sent ping");
}
}
function reconnect() {
console.log(`Attempting to reconnect in ${reconnectDelay}ms`);
setTimeout(() => {
connect();
reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay); // 指数退避
}, reconnectDelay);
}
connect(); // 启动连接
五、总结与注意事项
- WebSocket 帧结构是数据传输的基础,理解帧结构才能更好地理解 WebSocket 协议。
- Ping/Pong 机制是保持连接的有效手段,心跳检测是利用 Ping/Pong 机制进行连接保活的关键。
- 断线重连是保证服务可用性的重要措施,选择合适的重连策略可以提高系统的健壮性。
- 在实际应用中,还需要考虑一些其他的因素,例如:
- 网络环境: 不同的网络环境可能会影响连接的稳定性和速度。
- 服务器负载: 服务器的负载过高可能会导致连接断开。
- 防火墙: 防火墙可能会阻止 WebSocket 连接。
- 代理服务器: 代理服务器可能不支持 WebSocket 协议。
- 请注意,以上代码示例仅为演示目的,实际应用中需要进行更完善的错误处理和异常处理。
希望今天的讲座对大家有所帮助! 如果大家有什么问题,欢迎提问!