WebSocket

扫码查看

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

 本文将使用Python编写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. 客户端连接

1 <script type="text/javascript">
2     var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
3     ...
4 </script>

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

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 ...
12 ...
13 ...
14 conn.send('响应【握手】信息')

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

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

注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

请求【握手】信息为:

 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 ...
View Code

提取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 ...
View Code

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)

基于Python实现解包过程(未实现长内容)
基于Python实现解包过程(未实现长内容)

解包详细过程:

 1 0                   1                   2                   3
 2  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
 3 +-+-+-+-+-------+-+-------------+-------------------------------+
 4 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 5 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 6 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 7 | |1|2|3|       |K|             |                               |
 8 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 9 |     Extended payload length continued, if payload len == 127  |
10 + - - - - - - - - - - - - - - - +-------------------------------+
11 |                               |Masking-key, if MASK set to 1  |
12 +-------------------------------+-------------------------------+
13 | Masking-key (continued)       |          Payload Data         |
14 +-------------------------------- - - - - - - - - - - - - - - - +
15 :                     Payload Data continued ...                :
16 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
17 |                     Payload Data continued ...                |
18 +---------------------------------------------------------------+
View Code

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

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
View Code

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

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

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

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import json
import tornado.ioloop
import tornado.web
import tornado.websocket


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('index.html')


class ChatHandler(tornado.websocket.WebSocketHandler):
    # 用户存储当前聊天室用户
    waiters = set()
    # 用于存储历时消息
    messages = []

    def open(self):
        """
        客户端连接成功时,自动执行
        :return:
        """
        ChatHandler.waiters.add(self)
        uid = str(uuid.uuid4())
        self.write_message(uid)

        for msg in ChatHandler.messages:
            content = self.render_string('message.html', **msg)
            self.write_message(content)

    def on_message(self, message):
        """
        客户端连发送消息时,自动执行
        :param message:
        :return:
        """
        msg = json.loads(message)
        ChatHandler.messages.append(message)

        for client in ChatHandler.waiters:
            content = client.render_string('message.html', **msg)
            client.write_message(content)

    def on_close(self):
        """
        客户端关闭连接时,,自动执行
        :return:
        """
        ChatHandler.waiters.remove(self)


def run():
    settings = {
        'template_path': 'templates',
        'static_path': 'static',
    }
    application = tornado.web.Application([
        (r"/", IndexHandler),
        (r"/chat", ChatHandler),
    ], **settings)
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
    run()

app.py
app.py
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python聊天室</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="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">

    </div>

    <script src="/static/jquery-2.1.4.min.js"></script>
    <script type="text/javascript">
        $(function () {
            wsUpdater.start();
        });

        var wsUpdater = {
            socket: null,
            uid: null,
            start: function() {
                var url = "ws://127.0.0.1:8888/chat";
                wsUpdater.socket = new WebSocket(url);
                wsUpdater.socket.onmessage = function(event) {
                    console.log(event);
                    if(wsUpdater.uid){
                        wsUpdater.showMessage(event.data);
                    }else{
                        wsUpdater.uid = event.data;
                    }
                }
            },
            showMessage: function(content) {
                $('#container').append(content);
            }
        };

        function sendMsg() {
            var msg = {
                uid: wsUpdater.uid,
                message: $("#txt").val()
            };
            wsUpdater.socket.send(JSON.stringify(msg));
        }

</script>

</body>
</html>

index.html
inde.html

示例源码下载

12-22 20:32
查看更多