猿们,你真的了解WebSocket吗?

Posted 老男孩Linux

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了猿们,你真的了解WebSocket吗?相关的知识,希望对你有一定的参考价值。

猿们,你真的了解WebSocket吗?
SUMMER
猿们,你真的了解WebSocket吗?

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


 本文将使用Python编写Socket服务端,一步一步分析请求过程!!!



猿们,你真的了解WebSocket吗?
1. 启动服务端
猿们,你真的了解WebSocket吗?


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服务器后,等待用户【连接】,然后进行收发数据。



猿们,你真的了解WebSocket吗?
2. 客户端连接
猿们,你真的了解WebSocket吗?


<script type="text/javascript">

    var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");

    ...

</script>


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



猿们,你真的了解WebSocket吗?
3. 建立连接【握手】
猿们,你真的了解WebSocket吗?


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


请求【握手】信息为:

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('\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'))

...

...

...


猿们,你真的了解WebSocket吗?
4.客户端和服务端收发数据
猿们,你真的了解WebSocket吗?



客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的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[% 4]

        bytes_list.append(chunk)

    body = str(bytes_list, encoding='utf-8')

    print(body)


解包详细过程: 


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

+---------------------------------------------------------------+


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.

  2. Read the next 16 bits and interpret those as an unsigned integer. You're done.

  3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done.


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):



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.


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


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

 



猿们,你真的了解WebSocket吗?
5. 基于Python实现简单示例
猿们,你真的了解WebSocket吗?



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

#!/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[% 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类库实现客户端


<!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>

 


猿们,你真的了解WebSocket吗?
6. 基于Tornado框架实现Web聊天室
猿们,你真的了解WebSocket吗?



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


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


 猿们,你真的了解WebSocket吗?app.py

猿们,你真的了解WebSocket吗?index.html



注意:


也可以添加【果果姐】微信(life_kl0517)直接找果果姐要示例源码下载打包文件哦,另外还可以获得价值888元的“精品课程”一套~

长按二维码即可添加



更多IT技术性文章和福利活动

以上是关于猿们,你真的了解WebSocket吗?的主要内容,如果未能解决你的问题,请参考以下文章

你真的了解WebSocket吗?

你真的了解WebSocket吗?

你真的了解WebSocket吗?

你真的了解WebSocket吗?

你真的了解WebSocket吗?

你真的了解WebSocket吗?