zoukankan      html  css  js  c++  java
  • 异步通信----WebSocket

    什么是WebSocket?

    WebSocket API是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。WebSocket目前由W3C进行标准化。WebSocket已经受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等浏览器的支持。

    WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允许跨域通信。

    Ajax技术很聪明的一点是没有设计要使用的方式。WebSocket为指定目标创建,用于双向推送消息。

    WebSocket通信原理

            - 服务端(socket服务端)
                1. 服务端开启socket,监听IP和端口
                3. 允许连接
                * 5. 服务端接收到特殊值【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】
                * 6. 加密后的值发送给客户端
                            
            - 客户端(浏览器)
                2. 客户端发起连接请求(IP和端口)
                * 4. 客户端生成一个xxx,【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服务端发送一段特殊值
                * 7. 客户端接收到加密的值
    

      基于代码实现:

    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  #这个是API随机生成的
    • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
    • 将加密结果响应给客户端

      注:magic string为(亘古不变):258EAFA5-E914-47DA-95CA-C5AB0DC85B11

    请求【握手】信息为:

    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值并加密:

    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('
    '):
            print(i)
        header, body = data.split('
    
    ', 1)
        header_list = header.split('
    ')
        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
    " 
          "Upgrade:websocket
    " 
          "Connection: Upgrade
    " 
          "Sec-WebSocket-Accept: %s
    " 
          "WebSocket-Location: ws://%s%s
    
    "
    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服务端需要手动实现。

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

     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实现解包过程(未实现长内容)

    数据交互协议:

    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则该消息为消息尾部,如果为零则还有后续数据包;后面3位是用于扩展定义的,如果没有扩展约定的情况则必须为0.可以通过以下c#代码方式得到相应值
    
    
    mDataPackage.IsEof = (data[start] >> 7) > 0;
    最低4位用于描述消息类型,消息类型暂定有15种,其中有几种是预留设置.c#代码可以这样得到消息类型:
    
    int type = data[start] & 0xF;
    mDataPackage.Type = (PackageType)type;
    第二个字节
    
    消息的第二个字节主要用一描述掩码和消息长度,最高位用0或1来描述是否有掩码处理,可以通过以下c#代码方式得到相应值
    
    
    bool hasMask = (data[start] >>7) > 0;
    剩下的后面7位用来描述消息长度,由于7位最多只能描述127所以这个值会代表三种情况,一种是消息内容少于126存储消息长度,如果消息长度少于UINT16的情况此值为126,当消息长度大于UINT16的情况下此值为127;这两种情况的消息长度存储到紧随后面的byte[],分别是UINT16(2位byte)和UINT64(4位byte).可以通过以下c#代码方式得到相应值
    
    
    mPackageLength = (uint)(data[start] & 0x7F);
    start++;
    if (mPackageLength == 126)
    {
        mPackageLength = BitConverter.ToUInt16(data, start);
        start = start + 2;
    }
    else if (mPackageLength == 127)
    {
        mPackageLength = BitConverter.ToUInt64(data, start);
        start = start + 8;
    }
    如果存在掩码的情况下获取4位掩码值:
    
    
    if (hasMask)
    {
        mDataPackage.Masking_key = new byte[4];
        Buffer.BlockCopy(data, start, mDataPackage.Masking_key, 0, 4);
               
        start = start + 4;
        count = count - 4;
    }
    获取消息体
    
    当得到消息体长度后就可以获取对应长度的byte[],有些消息类型是没有长度的如%x8 denotes a connection close.对于Text类型的消息对应的byte[]是相应字符的UTF8编码.获取消息体还有一个需要注意的地方就是掩码,如果存在掩码的情况下接收的byte[]要做如下转换处理:
    
    
    if (mDataPackage.Masking_key != null)
        {
            int length = mDataPackage.Data.Count;
            for (var i = 0; i < length; i++)
                mDataPackage.Data.Array[i] = (byte)(mDataPackage.Data.Array[i] ^ mDataPackage.Masking_key[i % 4]);
        }
    

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

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

    基于Python实现简单示例

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

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

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

    源码见链接:点我下载

      

      

     

      

  • 相关阅读:
    codeforces 540D Bad Luck Island (概率DP)
    Codevs 1205 单词反转(Vector以及如何输出string)
    Codeforces 977D Divide by three, multiply by two(拓扑排序)
    Codeforces 977B Two-gram(stl之string掉进坑)
    HDU 6186 CS Course (连续位运算)
    HDU 1005 Number Sequence(矩阵快速幂,快速幂模板)
    HDU 1004 Let the Balloon Rise(STL初体验之map)
    2018天梯赛、蓝桥杯、(CCPC省赛、邀请赛、ICPC邀请赛)校内选拔赛反思总结!
    Newcoder Wannafly13 B Jxy军训(费马小定理、分数在模意义下的值)
    TCP的可靠传输(依赖流量控制、拥塞控制、连续ARQ)
  • 原文地址:https://www.cnblogs.com/chenice/p/6900702.html
Copyright © 2011-2022 走看看