1 WebSocket简介
写这个是因为前阵面试被问到,当时有点懵逼。。所以下来也简单学习一下。
原始的HTTP流程是一来一回的,这样导致很多业务没法开展或者开销很大,比如聊天室。虽然20年前就有聊天室了,但是当时是在网页上设置的定时轮询。比如2秒自动更新,这样的问题就导致服务器要频繁建立连接,而且每次都要带上整个HTTP头。
websocket的好处就是全双工,类似于底层的socket,建立了一次之后直接用就行了,不用在去建立TCP的链接,此外,每个数据包是二进制的,也不用发整个http头,减少了带宽需要。
对比项 | 传统轮询/长轮询 | WebSocket |
---|---|---|
连接开销 | 每次HTTP握手 | 1次握手,长连接 |
延迟 | 依赖轮询间隔(≥1秒) | 毫秒级 |
服务器资源 | 高(频繁连接/断开) | 低(少量持久连接) |
数据头开销 | 每次携带完整HTTP头 | 仅2~10字节帧头 |
底层实现我理解也不难,就是浏览器处理报文,然后底层调用系统的socket。
2 小实验
server.py
# server.py
import asyncio
import websockets
async def handle_connection(websocket):
print("网页客户端已连接")
try:
async for message in websocket:
print(f"收到网页消息: {message}")
reply = f"服务器回应: {message}"
await websocket.send(reply)
except websockets.exceptions.ConnectionClosedOK:
print("网页正常断开")
except Exception as e:
print(f"连接异常: {e}")
async def main():
async with websockets.serve(handle_connection, "0.0.0.0", 8765):
print("服务器启动,等待网页连接...")
await asyncio.Future() # 永久挂起
if __name__ == "__main__":
print("begin...")
asyncio.run(main())
client.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket网页客户端</title>
</head>
<body>
<h1>WebSocket 测试</h1>
<input type="text" id="messageInput" placeholder="输入发送内容">
<button onclick="sendMessage()">发送</button>
<div id="chatLog" style="margin-top:20px;"></div>
<script>
let socket = new WebSocket("ws://106.55.99.xx:8765");
socket.onopen = function() {
console.log("连接服务器成功");
document.getElementById('chatLog').innerHTML += "<p><em>已连接服务器</em></p>";
};
socket.onmessage = function(event) {
console.log("收到服务器消息: " + event.data);
document.getElementById('chatLog').innerHTML += "<p>服务器: " + event.data + "</p>";
};
socket.onclose = function(event) {
console.log("服务器连接关闭");
document.getElementById('chatLog').innerHTML += "<p><em>连接关闭</em></p>";
};
socket.onerror = function(error) {
console.log("连接错误: " + error.message);
document.getElementById('chatLog').innerHTML += "<p><em>连接错误</em></p>";
};
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value;
socket.send(message);
document.getElementById('chatLog').innerHTML += "<p>我: " + message + "</p>";
input.value = "";
}
</script>
</body>
</html>
server部署在腾讯云。(要注意打开tcp8765的防火墙)
客户端
3 协议分析
这段代码来自维基,很清楚的说明了websocket的流程。就是建立链接和数据发送两个部分。
from socket import socket
from base64 import b64encode
from hashlib import sha1
MAGIC = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
# Create socket and listen at port 80
ws = socket()
ws.bind(("", 80))
ws.listen()
conn, addr = ws.accept()
# Parse request
for line in conn.recv(4096).split(b"\r\n"):
if line.startswith(b"Sec-WebSocket-Key"):
nonce = line.split(b":")[1].strip()
# Format response
response = f"""\
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: {b64encode(sha1(nonce + MAGIC).digest()).decode()}
"""
conn.send(response.replace("\n", "\r\n").encode())
while True: # decode messages from the client
header = conn.recv(2)
FIN = bool(header[0] & 0x80) # bit 0
assert FIN == 1, "We only support unfragmented messages"
opcode = header[0] & 0xf # bits 4-7
assert opcode == 1 or opcode == 2, "We only support data messages"
masked = bool(header[1] & 0x80) # bit 8
assert masked, "The client must mask all frames"
payload_size = header[1] & 0x7f # bits 9-15
assert payload_size <= 125, "We only support small messages"
masking_key = conn.recv(4)
payload = bytearray(conn.recv(payload_size))
for i in range(payload_size):
payload[i] = payload[i] ^ masking_key[i % 4]
print(payload)
使用wireshark抓包如下:
3.1 建立连接
这一段是明文的,而且是文本,也没啥多看的,固定格式。
3.2 发送数据
第一个数据包的数据:
0000 c1 85 43 02 0a cf 71 36 3e cb 43 ..C...q6>.C
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| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
c1就是11000001,opcode是0001,表示是文本帧。
85就是10000101,第一个1表示是否加密。101表示长度是5。
后面的就是载荷。这里不详细看了。
收到的有一个ssh包,还有一个tcp包,内容是c1 18 7a 36 a7 f7 69 d7 c2 a7 33 57 3c 9d 3d e9 ae 29 56 0a 86 86 86 00 00。
也是c1开头,后面是一个18,表示加密了长度是8。
看起来,交互和返回的就是第一个PSH包和server的那个加密包
4 参考
https://en.wikipedia.org/wiki/WebSocket