【问题标题】:Websocket implementation in Python 3Python 3 中的 Websocket 实现
【发布时间】:2012-10-04 11:05:05
【问题描述】:

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

我的第一个倾向是使用已经存在的东西,来自 mod-pywebsocket 的示例应用程序被证明是有价值的。不幸的是,他们的 API 似乎不容易扩展,它是 Python2。

环顾博客圈,很多人已经为早期版本的 websocket 协议编写了​​自己的 websocket 服务器,但大多数人没有实现安全密钥哈希,所以不工作。

阅读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']

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

知道我错过了什么吗?还是当前工作的 Python3 websocket 示例?

【问题讨论】:

  • Tornado 同时支持 websockets 和 Python 3。tornadoweb.org/documentation/websocket.html
  • 谢谢托马斯。不过,我想先有一个独立的实现——这与理解协议和为我解决问题一样重要。查看tornado source code 我看到一个标头 Sec-WebSocket-Protocol 从服务器发送到客户端,但spec 表示这是可选的。
  • 如果客户端请求子协议,服务器应该回应它(总是假设它支持子协议)。否则会导致握手错误,因此这可能与您的消息发送问题无关。
  • 我看不出你的代码有什么问题。是否值得使用wireshark 来确认写入的数据与您在内部记录的数据相同?并且在握手和消息开始之间没有写入任何其他内容。
  • 是的,客户端没有请求子协议。但正如@Phillip 所说,我在握手回复后发送了额外的空格。

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


【解决方案1】:

当我尝试从 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 不喜欢标题中的尾随 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 异常。

总结。由于您的标头响应中有额外的 LF,您的 Python 代码无法与 Chrome 一起使用。还有其他事情发生,因为适用于 Chrome 的代码不适用于 Safari。

更新

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

base64.encodestring() 总是在其返回值中添加一个尾随 \n。这就是 Safari 抱怨的 LF 的来源。

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

【讨论】:

  • 太棒了,删除多余的 CRLF 后,它现在可以在 Firefox 和 Chrome 上使用了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-12
  • 2013-11-28
  • 2016-05-27
  • 1970-01-01
  • 1970-01-01
  • 2020-08-23
相关资源
最近更新 更多