WebSocket

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值