websocket内部原理简单分析

现在的主流浏览器一般都支持websocket,浏览器和服务端创建链接之后默认不断开,它的诞生能够真正的实现服务端给客户端推送消息。
本文只是对websocket的内部原理做简单的分析,实际使用中,并不是所有的后端框架都默认支持websocket协议,如果想使用的话,可能需要借助不同的第三方模块。

websocket实现原理可以分为两部分

  1. 握手环节(handshake): 并不是所有的服务端都支持websocket,所以用握手环节来验证服务端是否支持websocket
  2. 收发数据环节:数据解密

一、握手环节

image

浏览器访问服务端之后浏览器会立刻生成一个随机字符串
浏览器会将生成好的随机肌肤穿发送给服务端(基于HTTP协议,放在请求头中),并且自己也保留一份

服务端和客户端都会对该随机字符串做以下处理:

1.1 先拿随机字符串跟magic string(固定的字符串)做字符串的拼接
1.2 将拼接之后的结果做加密处理(sha1 + base64)

服务端将生成好的处理结果发送给浏览器(基于HTTP协议,放在响应头中)
浏览器接受服务端发送过来的随机字符串跟本地处理好的随机字符串作比对,如果一致,说明服务端支持websocket;如果不一致说明不支持。

二、收发数据环节

前提知识点:

  1. 基于网络传输数据都是二进制格式,在python中可以用bytes类型对应
  2. 进制换算

先读取第二个自己的后七位数据(payload)根据payload做不同的处理(根据七位数据换算成10进制,最大127,最小0)
=127:继续往后读取8个字节数据(数据报10个字节)
=126:继续往后读取2个字节数据(数据报4个字节)
<=125:不在往后读取(数据报2个字节)

上述操作完成后,会继续往后读取固定长度4个字节的数据(masking-key)
依据msking-key解析出真实数据


image

代码验证

# 请求头中的随机字符串
Sec-WebSocket-Key: NlNG/FK/FrQS/RH5Bcy9Gw==
# 响应头
tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
      "Upgrade:websocket\r\n" \
      "Connection: Upgrade\r\n" \
      "Sec-WebSocket-Accept: %s\r\n" \
      "WebSocket-Location: ws://127.0.0.1:8080\r\n\r\n"
response_str = tpl %ac.decode('utf-8')  # 处理到响应头中
import socket
import hashlib
import base64

# 正常的socket代码
sock = socket.socket()  # 默认就是TCP
# 避免mac本重启服务经常报地址被占用的错误
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen(5)


conn, address = sock.accept()
data = conn.recv(1024)  # 获取客户端发送的消息
# print(data.decode('utf-8'))

def get_headers(data):
    """
    将请求头格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict

def get_data(info):
    """
    按照websocket解密规则针对不同的数字进行不同的解密处理
    :param info:
    :return:
    """
    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:]
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]

    bytes_list = bytearray()
    for i in range(len(decoded)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')

    return body


header_dict = get_headers(data)  # 将一大堆请求头转换成字典数据  类似于wsgiref模块
client_random_string = header_dict['Sec-WebSocket-Key']  # 获取浏览器发送过来的随机字符串
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'  # 全球共用的随机字符串 一个都不能写错
value = client_random_string + magic_string  # 拼接
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())  # 加密处理


tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
      "Upgrade:websocket\r\n" \
      "Connection: Upgrade\r\n" \
      "Sec-WebSocket-Accept: %s\r\n" \
      "WebSocket-Location: ws://127.0.0.1:8080\r\n\r\n"
response_str = tpl %ac.decode('utf-8')  # 处理到响应头中


# 基于websocket收发消息
conn.send(bytes(response_str,encoding='utf-8'))

while True:
    data = conn.recv(1024)
    # print(data)  # 加密数据 b'\x81\x89\n\x94\xac#\xee)\x0c\xc6\xaf)I\xb6\x80'
    value = get_data(data)
    print(value)

前端

<script>
    var ws = new WebSocket('ws://127.0.0.1:8080/')
    // 这一句话帮你完成了握手环节所有的操作
    // 1 生成随机字符串
    // 2 对字符串做拼接和加码操作
    // 3 接受服务端返回的字符串做比对
</script>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容