sockets - Python 3 中的 Websocket 实现

标签 sockets python-3.x websocket mod-pywebsocket

尝试为 Python3 支持的应用程序创建 Web 前端。该应用程序将需要双向流,这听起来是研究 websocket 的好机会。

我的第一个倾向是使用已经存在的东西,并且 mod-pywebsocket 中的示例应用程序已被证明很有值(value)。不幸的是,他们的 API 似乎不太容易进行扩展,而且它是 Python2。

环顾博客圈,很多人都为早期版本的 websocket 协议(protocol)编写了​​自己的 websocket 服务器,大多数人没有实现安全 key 哈希,因此不起作用。

阅读RFC 6455我决定自己尝试一下,并得出以下结论:

#!/usr/bin/env python3

"""
A partial implementation of RFC 6455
http://tools.ietf.org/pdf/rfc6455.pdf
Brian Thorne 2012
"""
  
import socket
import threading
import time
import base64
import hashlib

def calculate_websocket_hash(key):
    magic_websocket_string = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    result_string = key + magic_websocket_string
    sha1_digest = hashlib.sha1(result_string).digest()
    response_data = base64.encodestring(sha1_digest)
    response_string = response_data.decode('utf8')
    return response_string

def is_bit_set(int_type, offset):
    mask = 1 << offset
    return not 0 == (int_type & mask)

def set_bit(int_type, offset):
    return int_type | (1 << offset)

def bytes_to_int(data):
    # note big-endian is the standard network byte order
    return int.from_bytes(data, byteorder='big')


def pack(data):
    """pack bytes for sending to client"""
    frame_head = bytearray(2)
    
    # set final fragment
    frame_head[0] = set_bit(frame_head[0], 7)
    
    # set opcode 1 = text
    frame_head[0] = set_bit(frame_head[0], 0)
    
    # payload length
    assert len(data) < 126, "haven't implemented that yet"
    frame_head[1] = len(data)
    
    # add data
    frame = frame_head + data.encode('utf-8')
    print(list(hex(b) for b in frame))
    return frame

def receive(s):
    """receive data from client"""
    
    # read the first two bytes
    frame_head = s.recv(2)
    
    # very first bit indicates if this is the final fragment
    print("final fragment: ", is_bit_set(frame_head[0], 7))
    
    # bits 4-7 are the opcode (0x01 -> text)
    print("opcode: ", frame_head[0] & 0x0f)
    
    # mask bit, from client will ALWAYS be 1
    assert is_bit_set(frame_head[1], 7)
    
    # length of payload
    # 7 bits, or 7 bits + 16 bits, or 7 bits + 64 bits
    payload_length = frame_head[1] & 0x7F
    if payload_length == 126:
        raw = s.recv(2)
        payload_length = bytes_to_int(raw)
    elif payload_length == 127:
        raw = s.recv(8)
        payload_length = bytes_to_int(raw)
    print('Payload is {} bytes'.format(payload_length))
    
    """masking key
    All frames sent from the client to the server are masked by a
    32-bit nounce value that is contained within the frame
    """
    masking_key = s.recv(4)
    print("mask: ", masking_key, bytes_to_int(masking_key))
    
    # finally get the payload data:
    masked_data_in = s.recv(payload_length)
    data = bytearray(payload_length)
    
    # The ith byte is the XOR of byte i of the data with
    # masking_key[i % 4]
    for i, b in enumerate(masked_data_in):
        data[i] = b ^ masking_key[i%4]

    return data

def handle(s):
    client_request = s.recv(4096)
    
    # get to the key
    for line in client_request.splitlines():
        if b'Sec-WebSocket-Key:' in line:
            key = line.split(b': ')[1]
            break
    response_string = calculate_websocket_hash(key)
    
    header = '''HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: {}\r
\r
'''.format(response_string)
    s.send(header.encode())
    
    # this works
    print(receive(s))
    
    # this doesn't
    s.send(pack('Hello'))
    
    s.close()

s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 9876))
s.listen(1)

while True:
    t,_ = s.accept()
    threading.Thread(target=handle, args = (t,)).start()

使用这个基本测试页面(与 mod-pywebsocket 一起使用):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Web Socket Example</title>
    <meta charset="UTF-8">
</head>
<body>
    <div id="serveroutput"></div>
    <form id="form">
        <input type="text" value="Hello World!" id="msg" />
        <input type="submit" value="Send" onclick="sendMsg()" />
    </form>
<script>
    var form = document.getElementById('form');
    var msg = document.getElementById('msg');
    var output = document.getElementById('serveroutput');
    var s = new WebSocket("ws://"+window.location.hostname+":9876");
    s.onopen = function(e) {
        console.log("opened");
        out('Connected.');
    }
    s.onclose = function(e) {
        console.log("closed");
        out('Connection closed.');
    }
    s.onmessage = function(e) {
        console.log("got: " + e.data);
        out(e.data);
    }
    form.onsubmit = function(e) {
        e.preventDefault();
        msg.value = '';
        window.scrollTop = window.scrollHeight;
    }
    function sendMsg() {
        s.send(msg.value);
    }
    function out(text) {
        var el = document.createElement('p');
        el.innerHTML = text;
        output.appendChild(el);
    }
    msg.focus();
</script>
</body>
</html>

这会接收数据并正确解除屏蔽,但我无法使传输路径正常工作。

作为向套接字写入“Hello”的测试,上面的程序计算要写入套接字的字节数:

['0x81', '0x5', '0x48', '0x65', '0x6c', '0x6c', '0x6f']

section 5.7 中给出的十六进制值匹配RFC 的。不幸的是,该框架从未出现在 Chrome 的开发者工具中。

知道我错过了什么吗?或者当前正在运行的 Python3 websocket 示例?

最佳答案

当我尝试在 Lion 上的 Safari 6.0.1 中与你的 python 代码对话时,我得到了

Unexpected LF in Value at ...

在 Javascript 控制台中。我还从 Python 代码中收到 IndexError 异常。

当我在 Lion 上的 Chrome 版本 24.0.1290.1 开发版中与您的 python 代码交谈时,我没有收到任何 Javascript 错误。在您的 javascript 中,会调用 onopen()onclose() 方法,但不会调用 onmessage()。 python 代码不会抛出任何异常,并且似乎已接收消息并发送了响应,即正是您所看到的行为。

由于 Safari 不喜欢 header 中的尾随 LF,我尝试将其删除,即

header = '''HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: {}\r
'''.format(response_string)

当我进行此更改时,Chrome 可以看到您的响应消息,即

got: Hello

显示在 JavaScript 控制台中。

Safari 仍然无法工作。现在,当我尝试发送消息时,它提出了一个不同的问题。

websocket.html:36 INVALID_STATE_ERR: DOM Exception 11: An attempt was made to use an object that is not, or is no longer, usable.

没有任何 javascript websocket 事件处理程序触发,而且我仍然看到来自 python 的 IndexError 异常。

总而言之。由于 header 响应中存在额外的 LF,您的 Python 代码无法在 Chrome 上运行。还有其他问题正在发生,因为适用于 Chrome 的代码不适用于 Safari。

更新

我已经解决了根本问题,现在示例可以在 Safari 和 Chrome 中运行。

base64.encodestring() 始终在其返回值中添加尾随 \n。这就是 Safari 提示的 LF 的来源。

calculate_websocket_hash 的返回值调用 .strip() 并使用原始 header 模板可以在 Safari 和 Chrome 上正常工作。

关于sockets - Python 3 中的 Websocket 实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12879274/

相关文章:

python - openpyxl 没有归属错误

javascript - Edge 浏览器中未定义的 PHP Websocket

boost-asio - 增强 Asio 和 Web 套接字?

sockets - 冲洗 socket 是什么意思?

c++ - 非阻塞套接字上的 select()、recv() 和 EWOULDBLOCK

php - 在php中使用Proxy获取url的内容,但不 curl

python - 使用Python 2.7/python-lzo 1.11解压缩MiniLZO字符串

python - 如何让 python 3.x 在网络浏览器中输入文本

Python 3.2 在 csv.DictReader 中跳过一行

javascript - 如何在 java websocket 服务器上读取通过 websocket 发送的 javascript BLOB 数据