Python实现WebSocket
Python实现WebSocket
一、WebSocket建立连接
1. 握手环节
-
目的:验证服务端是否支持Websocket协议
-
流程:
- 客户端浏览器第一次访问服务器的时候,浏览器内部会自动生成一个随机字符串,将该随机字符串发送给服务端(基于http)协议)浏览器也保留随机生成的字符串(在请求头里面)
- 服务端接收随机字符串之后,会将字符串与magic string(全球统一)做字符串拼接,然后利用加密算法对拼接好的字符串进行加密处理(sha1/base64),此时客户端也对产生的随机字符串做上述的拼接和加密操作
- 接着服务器将产生好的随机字符串发送给客户端的浏览器(响应头里面),客户端浏览器会对比服务器发送的随机字符串与浏览器本地操作的随机字符串进行对比,如果一致说明该服务端支持websocket,如果不一致服务端则不支持。
请求协议
GET / HTTP/1.1\r\n # 请求首行,握手阶段还是使用http协议
Host: 127.0.0.1:8080\r\n # 请求头
Connection: Upgrade\r\n # 表示要升级协议
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\n
Upgrade: websocket\r\n # 要升级协议到websocket协议
Origin: http://localhost:63342\r\n
Sec-WebSocket-Version: 13\r\n # 表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
Sec-WebSocket-Key: 07EWNDBSpegw1vfsIBJtkg==\r\n # 对应服务端响应头的Sec-WebSocket-Accept,由于没有同源限制,websocket客户端可任意连接支持websocket的服务。这个就相当于一个钥匙一把锁,避免多余的,无意义的连接
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n
- Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,发送给服务器
- 服务端从请求(HTTP的请求头)信息中提取 Sec-WebSocket-Key,利用magic_string 和 Sec-WebSocket-Key 先进行拼接,然后采用hmac1加密,再进行base64加密
- 将加密结果响应给客户端,服务器会返回下列东西,表示已经接受到请求, 成功建立 WebSocket
响应协议
HTTP/1.1 101 Switching Protocols\r\n # 响应首行,还是使用http协议
Upgrade:websocket\r\n # 表示要升级到websocket协议
Connection: Upgrade\r\n # 表示要升级协议
Sec-WebSocket-Accept: 07EWNDBSpegw1vfsIBJtkg==\r\n # 根据客户端请求首部的Sec-WebSocket-Key计算出来。
WebSocket-Location: ws://127.0.0.1:8000\r\n\r\n
2. 收发数据(send/onmessage)
验证成功之后就可以数据交互了 但是交互的数据是加密的 需要解密处理
- 数据基于网络传输都是二进制格式,单位换算 8bit = 1bytes
- 步骤一:读取第二个字节的后七位称之为payload,根据payload大小决定不同的处理方式:
- =127 再读取8个字节 作为数据报
- =126 再读取2个字节 作为数据报
- <=125 不再往后读了
- 步骤二:
# 步骤1之后 会对剩下的数据再读取4个字节(masking-key)
# 之后依据masking-key算出真实数据
var DECODED = "";
for(var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
二、Python实现
Python后端:
from django.test import TestCase
# Create your tests here.
import socket
import base64
import hashlib
# 正常的socket代码
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止linux/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) # 获取客户端发送的消息
def get_headers(data):
"""
将请求头格式化成字典
:param data:
:return:
"""
"""
请求头格式:
GET / HTTP/1.1\r\n # 请求首行,握手阶段还是使用http协议
Host: 127.0.0.1:8080\r\n # 请求头
Connection: Upgrade\r\n # 表示要升级协议
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\n
Upgrade: websocket\r\n # 要升级协议到websocket协议
Origin: http://localhost:63342\r\n
Sec-WebSocket-Version: 13\r\n # 表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
Sec-WebSocket-Key: 07EWNDBSpegw1vfsIBJtkg==\r\n # 对应服务端响应头的Sec-WebSocket-Accept,由于没有同源限制,websocket客户端可任意连接支持websocket的服务。这个就相当于一个钥匙一把锁,避免多余的,无意义的连接
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n
"""
header_dict = {}
data = str(data, encoding='utf-8')
header, body = data.split('\r\n\r\n', 1) # 因为请求头信息结尾都是\r\n,并且最后末尾部分是\r\n\r\n;
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
# 想将http协议的数据处理成字典的形式方便后续取值
header_dict = get_headers(data) # 将一大堆请求头转换成字典数据 类似于wsgiref模块
client_random_string = header_dict['Sec-WebSocket-Key'] # 获取浏览器发送过来的随机字符串
# magic string拼接
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' # 全球共用的随机字符串 一个都不能写错
# 确认握手Sec-WebSocket-Key固定格式:headers头部的Sec-WebSocket-Key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
# 确认握手的秘钥值为 传入的秘钥+magic_string,使用sha1算法加密,然后base64转码
value = client_random_string + magic_string # 拼接
# 算法加密 对请求头中的sec-websocket-key进行加密
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') # 处理到响应头中
# 将随机字符串给浏览器返回回去
print(f"建立连接,加密验证key{ac}")
conn.send(bytes(response_str, encoding='utf-8'))
def get_data(info):
"""
前后端进行通信,对前端发生消息进行解密
对返回消息进行解码比较复杂,详见数据帧格式解析
"""
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
# 基于websocket通信
while True:
# ws.send("info")
data = conn.recv(1024) # 数据是加密处理的
# print(data)
# 对data进行解密操作
value = get_data(data)
print(value)
前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<script>
var ws = new WebSocket('ws://127.0.0.1:8080/')
// 上面这句代码干了很多事
// 1 产生随机字符串并发送给服务端 Sec-WebSocket-Key: EMy5N4dwjl/jHoU0eYDDGQ==
// 2 服务端发送处理好的内容过来之后 自动校验
</script>
</body>
</html>
前端后端发送消息
三、总结
客户端第一次访问服务端,客户端会随机生成一个字符串发送给服务端Sec-WebSocket-Key,浏览器也会保存一份,然后服务端解析获取随机字符串与magic string进行拼接,然后通过base64和sha1进行加密,返回给浏览器,浏览器保留的字符串也会经过上面操作之后与后端生成的加密字符串进行比较,如果相同则说明服务端支持websocket,如果不一致则不支持(握手环节)。验证成功之后,就可以数据交互,交互的数据是加密的,所以需要解密,解密:首先读取第二个字节后七位,后七位也称之为payload,根据payload的大小进行不同的操作,如果 =127 再读取后面8个字节作为数据报,如果=126在读取后面2个字节作为数据报,如果<125 则不再往后读取数据,最后会对剩下的数据再次读取4个字节之后,依据masking-key 算出(异或)真实的数据结果。
在当下的阶段,必将由程序员来主导,甚至比以往更甚。