WebsSocket
一、Websocket原理(握手、解密、加密)
WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
本文将使用Python编写Socket服务端,一步一步分析请求过程!!!
1、启动服务端
|
1
2
3
4
5
6
7
8
9
10
11
|
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(('127.0.0.1', 9527))sock.listen(5)# 获取客户端socket对象,并等待用户连接conn, address = sock.accept().................. |
启动Socket服务器后,等待用户【连接】,然后进行收发数据。
2、客户端连接
|
1
2
3
4
|
<script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:9527/xxoo"); ......</script> |
当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!
3、建立连接【握手】
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(('127.0.0.1', 9527))sock.listen(5)# 获取客户端socket对象,并等待用户连接conn, address = sock.accept()# 获取客户端的【握手】信息,data即握手信息data = conn.recv(1024)............conn.send('响应【握手】信息') |
请求和响应的【握手】信息需要遵循规则:
- 从请求【握手】信息中提取 Sec-WebSocket-Key;
- 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密;
- 将加密结果响应给客户端;
注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
请求【握手】信息为:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
b'GET /xxoo HTTP/1.1\r\nHost: 127.0.0.1:9527\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36\r\nUpgrade: websocket\r\nOrigin: http://localhost:63342\r\nSec-WebSocket-Version: 13\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: session=ba01367c-59b9-41d4-81ba-30b70db282c6\r\nSec-WebSocket-Key: jLSLU57WxRJACRQxlN47Tw==\r\nSec-WebSocket-Extensions: permessage-deflate; ......' |
提取Sec-WebSocket-Key值并加密:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import socketimport base64import hashlibdef get_headers(data): """ 将请求头格式化成字典 :param data: 请求头 :return: 请求头信息字典 """ header_dict = {} header_str = data.decode("utf8") for i in header_str.split("\r\n"): if str(i).startswith("Sec-WebSocket-Key"): header_dict["Sec-WebSocket-Key"] = i.split(":")[1].strip() return header_dictsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(('127.0.0.1', 9527))sock.listen(5)# 获取客户端socket对象,并等待用户连接conn, address = sock.accept()# 获取客户端的【握手】信息data = conn.recv(1024)headers = get_headers(data) # 提取请求头信息# 对请求头中的Sec-WebSocket-Key进行加密response_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:9527\r\n\r\n"magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'value = headers['Sec-WebSocket-Key'] + magic_stringac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())response_str = response_tpl % (ac.decode('utf-8'))# 响应【握手】信息conn.send(response_str.encode("utf-8"))............ |
至此,客户端与服务端完成握手过程。
4、客户端与服务端收发数据
客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。
第一步:获取客户端发送的数据【解包】
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
# b'\x81\x86#\xa47\x93\xc7\x19\x97v\x86\x19' 你好hashstr = b'\x81\x83\xceH\xb6\x85\xffz\x85'# b'\x81 \x86# \xa47\x93\xc7\x19 \x97v\x86\x19'# 将第二个字节 \x86# 也就是第9-16位 与 127 进行位运算payload_len = hashstr[1] & 127if payload_len == 127: extend_payload_len = hashstr[2:10] mask = hashstr[10:14] decoded = hashstr[14:]# 当位运算结果等于127时,则第3-10个字节为数据长度# 第11-14字节为mask ,即解密所需字符串# 则数据为第15字节至结尾if payload_len == 126: extend_payload_len = hashstr[2:4] mask = hashstr[4:8] decoded = hashstr[8:]# 当位运算结果等于126时,则第3-4个字节为数据长度# 第5-8字节为mask ,即解密所需字符串# 则数据为第9字节至结尾if payload_len <= 125: extend_payload_len = None mask = hashstr[2:6] decoded = hashstr[6:]# 当位运算结果小于等于125时,则这个数字就是数据的长度# 第3-6字节为mask ,即解密所需字符串# 则数据为第7字节至结尾str_byte = bytearray()for i in range(len(decoded)): # 循环数据长度 byte = decoded[i] ^ mask[i % 4] # ^ 是或运算 str_byte.append(byte)print(str_byte.decode("utf8")) |
解包详细过程:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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| | |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +| Extended payload length continued, if payload len == 127 |+ - - - - - - - - - - - - - - - +-------------------------------+| |Masking-key, if MASK set to 1 |+-------------------------------+-------------------------------+| Masking-key (continued) | Payload Data |+-------------------------------- - - - - - - - - - - - - - - - +: Payload Data continued ... :+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +| Payload Data continued ... |+---------------------------------------------------------------+ |
第二步:向客户端发送数据【封包】
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length == 126: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True |
二、基于Python实现简单示例
1、基于Python socket实现的WebSocket服务端
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# !/usr/bin/env python# -*- coding:utf-8 -*-import socketimport base64import hashlibdef get_headers(data): """ 将请求头格式化成字典 :param data: 请求头 :return: 请求头信息字典 """ header_dict = {} header_str = data.decode("utf8") for i in header_str.split("\r\n"): if str(i).startswith("Sec-WebSocket-Key"): header_dict["Sec-WebSocket-Key"] = i.split(":")[1].strip() return header_dictdef send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length == 126: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return Truedef run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 9527)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) headers = get_headers(data) # 对请求头中的Sec-WebSocket-Key进行加密 response_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:9527\r\n\r\n" magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) response_str = response_tpl % (ac.decode('utf-8')) # 响应【握手】信息 conn.send(response_str.encode("utf-8")) while True: try: info = conn.recv(8096) except Exception as e: info = None if not info: break 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') send_msg(conn, body.encode('utf-8')) sock.close()if __name__ == '__main__': run() |
2、利用JavaScript类库实现客户端
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
<!DOCTYPE html><html><head lang="en"> <meta charset="UTF-8"> <title></title></head><body> <div> <input type="text" id="txt"/> <input type="button" id="btn" value="提交" onclick="sendMsg();"/> <input type="button" id="close" value="关闭连接" onclick="closeConn();"/> </div> <div id="content"></div><script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:9527/chatsocket"); socket.onopen = function () { /* 与服务器端连接成功后,自动执行 */ var newTag = document.createElement('div'); newTag.innerHTML = "【连接成功】"; document.getElementById('content').appendChild(newTag); }; socket.onmessage = function (event) { /* 服务器端向客户端发送数据时,自动执行 */ var response = event.data; var newTag = document.createElement('div'); newTag.innerHTML = response; document.getElementById('content').appendChild(newTag); }; socket.onclose = function (event) { /* 服务器端主动断开连接时,自动执行 */ var newTag = document.createElement('div'); newTag.innerHTML = "【关闭连接】"; document.getElementById('content').appendChild(newTag); }; function sendMsg() { var txt = document.getElementById('txt'); socket.send(txt.value); txt.value = ""; } function closeConn() { socket.close(); var newTag = document.createElement('div'); newTag.innerHTML = "【关闭连接】"; document.getElementById('content').appendChild(newTag); }</script></body></html> |
参考博客(一):http://www.runoob.com/html/html5-websocket.html
参考博客(二):https://www.cnblogs.com/fuqiang88/p/5956363.html
参考博客(三):https://baijiahao.baidu.com/s?id=1617829980923673614&wfr=spider&for=pc

浙公网安备 33010602011771号