WebSocket

发布时间 2023-12-08 15:17:42作者: 木屐呀

 WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。

Http连接和Websocket的区别:

- Http,socket实现,短链接,请求响应
- WebSocket,socket实现,双工通道,请求响应,推送,socket创建连接,不断开

1. 启动服务端

1 import socket
2 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
4 sock.bind(('127.0.0.1', 8002))
5 sock.listen(5)
6 # 等待用户连接
7 conn, address = sock.accept()
8 ...

启动Socket服务器后,等待用户【连接】,然后进行收发数据。

2. 客户端连接

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8     <script type="text/javascript">
 9         ws = new WebSocket('ws://127.0.0.1:8002');
10     </script>
11 </body>
12 </html>

当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!

3. 建立连接【握手】

 1 import socket
 2  
 3 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 4 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 5 sock.bind(('127.0.0.1', 8002))
 6 sock.listen(5)
 7 # 获取客户端socket对象
 8 conn, address = sock.accept()
 9 # 获取客户端的【握手】信息
10 data = conn.recv(1024)
11 print(data)
12 
13 conn.send(bytes('响应【握手】信息',encoding='utf-8'))

请求和响应的【握手】信息需要遵循规则:

  • 从请求【握手】信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端

请求握手信息如下:

 1 GET /chatsocket HTTP/1.1
 2 Host: 127.0.0.1:8002
 3 Connection: Upgrade
 4 Pragma: no-cache
 5 Cache-Control: no-cache
 6 Upgrade: websocket
 7 Origin: http://localhost:63342
 8 Sec-WebSocket-Version: 13
 9 Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
10 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
11 ...
12 ...

提取Sec-WebSocket-Key值并加密:

 1 import socket
 2 import base64
 3 import hashlib
 4  
 5 def get_headers(data):
 6     """
 7     将请求头格式化成字典
 8     :param data:
 9     :return:
10     """
11     header_dict = {}
12     data = str(data, encoding='utf-8')
13  
14     for i in data.split('\r\n'):
15         print(i)
16     header, body = data.split('\r\n\r\n', 1)
17     header_list = header.split('\r\n')
18     for i in range(0, len(header_list)):
19         if i == 0:
20             if len(header_list[i].split(' ')) == 3:
21                 header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
22         else:
23             k, v = header_list[i].split(':', 1)
24             header_dict[k] = v.strip()
25     return header_dict
26  
27  
28 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
29 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
30 sock.bind(('127.0.0.1', 8002))
31 sock.listen(5)
32  
33 conn, address = sock.accept()
34 data = conn.recv(1024)
35 headers = get_headers(data) # 提取请求头信息
36 # 对请求头中的sec-websocket-key进行加密
37 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
38       "Upgrade:websocket\r\n" \
39       "Connection: Upgrade\r\n" \
40       "Sec-WebSocket-Accept: %s\r\n" \
41       "WebSocket-Location: ws://%s%s\r\n\r\n"
42 magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
43 value = headers['Sec-WebSocket-Key'] + magic_string
44 ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
45 response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
46 # 响应【握手】信息
47 conn.send(bytes(response_str, encoding='utf-8'))
48 ...
49 ...
50 ...

白话建立连接(握手):

 1 - 服务端(socket服务端)
 2     1. 服务端开启socket,监听IP和端口
 3     3. 允许连接
 4     *4. 服务端接收到特殊值【加密shal,特殊值,magic string=“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”】
 5     *5. 加密后的值发送给客户端
 6             
 7 - 客户端(浏览器)
 8     2. 客户端发起连接请求(IP和端口)
 9     *2.1. 客户端生成一个xxx,【加密shal,特殊值,magic string=“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”】,向服务端发送一段特殊值(未加密的)
10     *6. 客户端接收到加密的值(与自己加密的比较)

4.客户端和服务端收发数据

客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。

 位运算基础:

# 1.位运算,右移动 >>
#     10010001
#     右4: 00001001
#
#     左4: 100100010000
# 2.异或运算
#     都是1:1
#     0,1: 0
#     0,0: 0

解包详细过程:

 1 # 对接收信息进行解包
 2 info = conn.recv(8096)
 3 info = b'\x81\x85\xcd\x1a\xab\x82\xa5\x7f\xc7\xee\xa2'
 4 
 5 #info[0]代表第一个字节
 6 
 7 ppcode = info[0] & 15(00001111)  #与00001111作与运算
 8 fin = info[0] >> 7 向右移7位  #向右1位得到FIN
 9 
10 #info[1]代表第二个字节
11 
12 payload_len = info[1] & 127(最大01111111) -》》64 32 16 8 4 2 1 = 127
13 
14 Extended payload length占位大小取决于Payload len ->> 数据头占位是payload_len的长度就足够还是需要扩展
15 
16 举例:
17 #if payload_len<126:
18 #    pass
19 #elif payload_len == 126:
20 #    pass
21 #else:
22 #    pass
23 
24     
25 ########################## 数据头部分 ##########################
26  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
27 +-+-+-+-+-------+-+-------------+-------------------------------+
28 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
29 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
30 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
31 | |1|2|3|       |K|             |                               |
32 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
33 |     Extended payload length continued, if payload len == 127  |
34 + - - - - - - - - - - - - - - - +-------------------------------+
35 |                               |Masking-key, if MASK set to 1  |
36 +-------------------------------+-------------------------------+
37 | Masking-key (continued)       |          Payload Data         |
38 +-------------------------------- - - - - - - - - - - - - - - - +
39 :                     Payload Data continued ...                :
40 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
41 |                     Payload Data continued ...                |
42 +---------------------------------------------------------------+

The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.

The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.

The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.

Decoding Payload Length

To read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:

  1. Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3.  #若Payload_len小于等于125,则请求头的占位大小就是它的长度(2个字节)
  2. Read the next 16 bits and interpret those as an unsigned integer. You're done. #若Payload_len的长度是126,则在此基础上extend 2个字节
  3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done. #若Payload_len的长度是127,则在此基础上再extend 8个字节

Reading and Unmasking the Data

If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):

 #### 若mask bit被设置,则数据往后读取4个字节作为masking key,通过masking key给数据部分进行解密

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

 

Now you can figure out what DECODED means depending on your application.

第一步:获取客户端发送的数据【解包】

 1 info = conn.recv(8096)
 2 
 3     payload_len = info[1] & 127
 4     if payload_len == 126:
 5         extend_payload_len = info[2:4]
 6         mask = info[4:8]
 7         decoded = info[8:]
 8     elif payload_len == 127:
 9         extend_payload_len = info[2:10]
10         mask = info[10:14]
11         decoded = info[14:]
12     else:
13         extend_payload_len = None
14         mask = info[2:6]
15         decoded = info[6:]
16 
17     bytes_list = bytearray()
18     for i in range(len(decoded)):
19         chunk = decoded[i] ^ mask[i % 4]
20         bytes_list.append(chunk)
21     body = str(bytes_list, encoding='utf-8')
22     print(body)
基于Python实现解包过程(未实现长内容)

第二步:向客户端发送数据【封包】

 1 def send_msg(conn, msg_bytes):
 2     """
 3     WebSocket服务端向客户端发送消息
 4     :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
 5     :param msg_bytes: 向客户端发送的字节
 6     :return: 
 7     """
 8     import struct
 9 
10     token = b"\x81"
11     length = len(msg_bytes)
12     if length < 126:
13         token += struct.pack("B", length)
14     elif length <= 0xFFFF:
15         token += struct.pack("!BH", 126, length)
16     else:
17         token += struct.pack("!BQ", 127, length)
18 
19     msg = token + msg_bytes
20     conn.send(msg)
21     return True
发送数据

5. 基于Python实现简单示例

a. 基于Python socket实现的WebSocket服务端:

  1 #!/usr/bin/env python
  2 # -*- coding:utf-8 -*-
  3 import socket
  4 import base64
  5 import hashlib
  6  
  7  
  8 def get_headers(data):
  9     """
 10     将请求头格式化成字典
 11     :param data:
 12     :return:
 13     """
 14     header_dict = {}
 15     data = str(data, encoding='utf-8')
 16  
 17     header, body = data.split('\r\n\r\n', 1)
 18     header_list = header.split('\r\n')
 19     for i in range(0, len(header_list)):
 20         if i == 0:
 21             if len(header_list[i].split(' ')) == 3:
 22                 header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
 23         else:
 24             k, v = header_list[i].split(':', 1)
 25             header_dict[k] = v.strip()
 26     return header_dict
 27  
 28  
 29 def send_msg(conn, msg_bytes):
 30     """
 31     WebSocket服务端向客户端发送消息
 32     :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
 33     :param msg_bytes: 向客户端发送的字节
 34     :return:
 35     """
 36     import struct
 37  
 38     token = b"\x81"
 39     length = len(msg_bytes)
 40     if length < 126:
 41         token += struct.pack("B", length)
 42     elif length <= 0xFFFF:
 43         token += struct.pack("!BH", 126, length)
 44     else:
 45         token += struct.pack("!BQ", 127, length)
 46  
 47     msg = token + msg_bytes
 48     conn.send(msg)
 49     return True
 50  
 51  
 52 def run():
 53     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 54     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 55     sock.bind(('127.0.0.1', 8003))
 56     sock.listen(5)
 57  
 58     conn, address = sock.accept()
 59     data = conn.recv(1024)
 60     headers = get_headers(data)
 61     response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
 62                    "Upgrade:websocket\r\n" \
 63                    "Connection:Upgrade\r\n" \
 64                    "Sec-WebSocket-Accept:%s\r\n" \
 65                    "WebSocket-Location:ws://%s%s\r\n\r\n"
 66  
 67     value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
 68     ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
 69     response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
 70     conn.send(bytes(response_str, encoding='utf-8'))
 71  
 72     while True:
 73         try:
 74             info = conn.recv(8096)
 75         except Exception as e:
 76             info = None
 77         if not info:
 78             break
 79         payload_len = info[1] & 127
 80         if payload_len == 126:
 81             extend_payload_len = info[2:4]
 82             mask = info[4:8]
 83             decoded = info[8:]
 84         elif payload_len == 127:
 85             extend_payload_len = info[2:10]
 86             mask = info[10:14]
 87             decoded = info[14:]
 88         else:
 89             extend_payload_len = None
 90             mask = info[2:6]
 91             decoded = info[6:]
 92  
 93         bytes_list = bytearray()
 94         for i in range(len(decoded)):
 95             chunk = decoded[i] ^ mask[i % 4]
 96             bytes_list.append(chunk)
 97         body = str(bytes_list, encoding='utf-8')
 98         send_msg(conn,body.encode('utf-8'))
 99  
100     sock.close()
101  
102 if __name__ == '__main__':
103     run()
View Code

b. 利用JavaScript类库实现客户端

 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6 </head>
 7 <body>
 8     <div>
 9         <input type="text" id="txt"/>
10         <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
11         <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
12     </div>
13     <div id="content"></div>
14  
15 <script type="text/javascript">
16     var socket = new WebSocket("ws://127.0.0.1:8003/chatsocket");
17  
18     socket.onopen = function () {
19         /* 与服务器端连接成功后,自动执行 */
20  
21         var newTag = document.createElement('div');
22         newTag.innerHTML = "【连接成功】";
23         document.getElementById('content').appendChild(newTag);
24     };
25  
26     socket.onmessage = function (event) {
27         /* 服务器端向客户端发送数据时,自动执行 */
28         var response = event.data;
29         var newTag = document.createElement('div');
30         newTag.innerHTML = response;
31         document.getElementById('content').appendChild(newTag);
32     };
33  
34     socket.onclose = function (event) {
35         /* 服务器端主动断开连接时,自动执行 */
36         var newTag = document.createElement('div');
37         newTag.innerHTML = "【关闭连接】";
38         document.getElementById('content').appendChild(newTag);
39     };
40  
41     function sendMsg() {
42         var txt = document.getElementById('txt');
43         socket.send(txt.value);
44         txt.value = "";
45     }
46     function closeConn() {
47         socket.close();
48         var newTag = document.createElement('div');
49         newTag.innerHTML = "【关闭连接】";
50         document.getElementById('content').appendChild(newTag);
51     }
52  
53 </script>
54 </body>
55 </html>
View Code

6. 基于Tornado框架实现Web聊天室

Tornado是一个支持WebSocket的优秀框架,其内部原理正如1~5步骤描述,当然Tornado内部封装功能更加完整。

以下是基于Tornado实现的聊天室示例:

 1 mport tornado.ioloop
 2 import tornado.web
 3 import tornado.websocket
 4 import uuid
 5 
 6 class IndexHandler(tornado.web.RequestHandler):
 7     def get(self,*args,**kwargs):
 8         self.render('index.html')
 9 
10 
11 users = set()
12 
13 class ChatHandler(tornado.websocket.WebSocketHandler):
14     def open(self,*args,**kwargs):
15         """
16         客户端和服务端已经建立连接
17         :param args:
18         :param kwargs:
19         :return:
20         """
21         print('xxx进入聊天室')
22         users.add(self)
23         self.name = uuid.uuid4()
24 
25     def on_message(self, message):
26         """
27         接收来自客户端发来的信息
28         :param message:
29         :return:
30         """
31         content = self.render_string('message.html',name=self.name,msg=message) #渲染
32         #发送消息给客户端
33         for client in users:
34             client.write_message(content)    #发送给多个对象
35 
36         # self.write_message(message)  单个对象发送
37 
38     def on_close(self):
39         """
40         客户端主动关闭连接
41         :return:
42         """
43         users.remove(self)
44 
45 def run():
46     settings = {
47         'template_path': 'templates',
48         'static_path': 'static',
49     }
50     application = tornado.web.Application([
51         (r"/", IndexHandler),
52         (r"/chat", ChatHandler),
53     ], **settings)
54     application.listen(8888)
55     tornado.ioloop.IOLoop.instance().start()
56 
57 if __name__ == "__main__":
58     run()
app.py
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6     <style>
 7         .container{
 8             border: 2px solid #dddddd;
 9             height: 400px;
10             overflow: auto;
11         }
12     </style>
13 </head>
14 <body>
15     <div style="width: 750px;margin: 0 auto;">
16         <h1>1024聊天室</h1>
17         <div class="container"></div>
18         <div class="input">
19             <input type="text" id="txt">
20             <input type="button" value="发送" id="btn" onclick="sendMessage()">
21         </div>
22     </div>
23     <script src="/static/jquery-3.3.1.min.js"></script>
24     <script>
25         ws = new WebSocket("ws://127.0.01:8888/chat");
26         // 服务器端向客户端发送数据时,自动执行
27         ws.onmessage = function (event) {
28             console.log(event.data);
29             $('.container').append(event.data);
30         };
31         // 服务器端主动断开连接时,自动执行
32         ws.onclose = function (event) {
33 
34         };
35         // 客户端发送数据
36         function sendMessage() {
37             ws.send($('#txt').val())
38         }
39     </script>
40 </body>
41 </html>
index.html
1 <div style="margin: 10px;background-color: bisque">{{name}}:{{msg}}</div>
message.html

其他扩展示例:

 1 import uuid
 2 import json
 3 import tornado.ioloop
 4 import tornado.web
 5 import tornado.websocket
 6 
 7 
 8 class IndexHandler(tornado.web.RequestHandler):
 9     def get(self):
10         self.render('index.html')
11 
12 
13 class ChatHandler(tornado.websocket.WebSocketHandler):
14     # 用户存储当前聊天室用户
15     waiters = set()
16     # 用于存储历时消息
17     messages = []
18 
19     def open(self):
20         """
21         客户端连接成功时,自动执行
22         :return: 
23         """
24         ChatHandler.waiters.add(self)
25         uid = str(uuid.uuid4())
26         self.write_message(uid)
27 
28         for msg in ChatHandler.messages:
29             content = self.render_string('message.html', **msg)
30             self.write_message(content)
31 
32     def on_message(self, message):
33         """
34         客户端连发送消息时,自动执行
35         :param message: 
36         :return: 
37         """
38         msg = json.loads(message)
39         ChatHandler.messages.append(message)
40 
41         for client in ChatHandler.waiters:
42             content = client.render_string('message.html', **msg)
43             client.write_message(content)
44 
45     def on_close(self):
46         """
47         客户端关闭连接时,,自动执行
48         :return: 
49         """
50         ChatHandler.waiters.remove(self)
51 
52 
53 def run():
54     settings = {
55         'template_path': 'templates',
56         'static_path': 'static',
57     }
58     application = tornado.web.Application([
59         (r"/", IndexHandler),
60         (r"/chat", ChatHandler),
61     ], **settings)
62     application.listen(8888)
63     tornado.ioloop.IOLoop.instance().start()
64 
65 
66 if __name__ == "__main__":
67     run()
app.py
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Python聊天室</title>
 6 </head>
 7 <body>
 8     <div>
 9         <input type="text" id="txt"/>
10         <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
11         <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
12     </div>
13     <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">
14 
15     </div>
16 
17     <script src="/static/jquery-2.1.4.min.js"></script>
18     <script type="text/javascript">
19         $(function () {
20             wsUpdater.start();
21         });
22 
23         var wsUpdater = {
24             socket: null,
25             uid: null,
26             start: function() {
27                 var url = "ws://127.0.0.1:8888/chat";
28                 wsUpdater.socket = new WebSocket(url);
29                 wsUpdater.socket.onmessage = function(event) {
30                     console.log(event);
31                     if(wsUpdater.uid){
32                         wsUpdater.showMessage(event.data);
33                     }else{
34                         wsUpdater.uid = event.data;
35                     }
36                 }
37             },
38             showMessage: function(content) {
39                 $('#container').append(content);
40             }
41         };
42 
43         function sendMsg() {
44             var msg = {
45                 uid: wsUpdater.uid,
46                 message: $("#txt").val()
47             };
48             wsUpdater.socket.send(JSON.stringify(msg));
49         }
50 
51 </script>
52 
53 </body>
54 </html>
index.html

参考博客:https://www.cnblogs.com/wupeiqi/p/6558766.html

参考文献:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers