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 socket 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 ) # 获取客户端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 socket 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 ) # 获取客户端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\n Host: 127.0 . 0.1 : 9527 \r\n Connection: Upgrade\r\n Pragma: no - cache\r\n Cache - Control: no - cache\r\n User - 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\n Upgrade: websocket\r\n Origin: http: / / localhost: 63342 \r\n Sec - WebSocket - Version: 13 \r\n Accept - Encoding: gzip, deflate, br\r\n Accept - Language: zh - CN,zh;q = 0.9 \r\n Cookie: session = ba01367c - 59b9 - 41d4 - 81ba - 30b70db282c6 \r\n Sec - WebSocket - Key: jLSLU57WxRJACRQxlN47Tw = = \r\n Sec - 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 socket import base64 import hashlib def 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_dict 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 ) # 获取客户端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_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" )) ...... ...... |
至此,客户端与服务端完成握手过程。
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 ] & 127 if 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 socket import base64 import hashlib def 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_dict 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 def 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