WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
1,启动服务端
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‘, 8002)) sock.listen(5) # 等待用户连接 conn, address = sock.accept() ... ... ...
启动Socket服务器后,等待用户【连接】,然后进行收发数据。
2,浏览器(客户端)连接
<script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:8002/xxoo"); ... </script>
当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!
3,建立连接(握手)
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‘, 8002)) sock.listen(5) # 获取客户端socket对象 conn, address = sock.accept() # 获取客户端的【握手】信息 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
|
GET / chatsocket HTTP / 1.1 Host: 127.0 . 0.1 : 8002 Connection: Upgrade Pragma: no - cache Cache - Control: no - cache Upgrade: websocket Origin: http: / / localhost: 63342 Sec - WebSocket - Version: 13 Sec - WebSocket - Key: mnwFxiOlctXFN / DeMt1Amg = = Sec - WebSocket - Extensions: permessage - deflate; client_max_window_bits ... ... |
提取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
44
45
46
47
48
49
50
|
import socket import base64 import hashlib def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str (data, encoding = ‘utf-8‘ ) for i in data.split( ‘\r\n‘ ): print (i) 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 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) sock.bind(( ‘127.0.0.1‘ , 8002 )) 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://%s%s\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‘ ), headers[ ‘Host‘ ], headers[ ‘url‘ ]) # 响应【握手】信息 conn.send(bytes(response_str, encoding = ‘utf-8‘ )) ... ... ... |
4,客户端与服务端收发数据
客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。
第一步:获取客户端发送的数据【解包】
info = conn.recv(8096) 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‘) print(body)
解包详细过程:
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 ... | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |
第二步:向客户端发送数据【封包】
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 <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True
5,基于Python实现简单示例
a. 基于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
101
102
103
|
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import base64 import hashlib 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 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 < = 0xFFFF : 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‘ , 8003 )) sock.listen( 5 ) conn, address = sock.accept() data = conn.recv( 1024 ) headers = get_headers(data) 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://%s%s\r\n\r\n" value = headers[ ‘Sec-WebSocket-Key‘ ] + ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11‘ ac = base64.b64encode(hashlib.sha1(value.encode( ‘utf-8‘ )).digest()) response_str = response_tpl % (ac.decode( ‘utf-8‘ ), headers[ ‘Host‘ ], headers[ ‘url‘ ]) conn.send(bytes(response_str, encoding = ‘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() |
b. 利用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:8003/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> |
c.更多
名称 类型 描述 # WebSocket 对象 提供到远程主机的双向通道。 # close 方法 关闭websocket。 # send 方法 使用websocket 发送数据到服务器。 # binaryType 属性 由 onmessage 接收的二进制数据格式。 # bufferedAmount 属性 使用 send 的已排队的数据字节数。 # extensions 属性 报告服务器所选中的扩展名。 # onclose 属性 当套接字关闭时调用的事件处理程序。 # onerror 属性 当出现错误时调用的事件处理程序。 # onmessage 属性 通知接收到消息的事件处理程序。 # onopen 属性 当 websocket 已连接时调用的事件处理程序。 # protocol 属性 报告服务器所选中的协议。 # readyState 属性 报告 websocket 连接的状态。 # url 属性 报告套接字的当前 URL。
7,基于Tornado实现Web聊天室
更新中...