Реализация Websocket в Python 3

попытка создать веб-интерфейс для приложения, поддерживаемого Python3. Приложение потребует двунаправленной потоковой передачи, которая звучала как хорошая возможность заглянуть в websockets.

моей первой склонностью было использовать что-то уже существующее, а пример приложения из 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 Protocolsr
Upgrade: websocketr
Connection: Upgrader
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>

Это получает данные и demasks это правильно, но я не могу передать путь к работа.

в качестве теста для записи "Hello" в сокет программа выше вычисляет байты, которые будут записаны в сокет как:

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

которые соответствуют шестнадцатеричным значениям, приведенным в 5.7 RFC. К сожалению, рамка не появляется в инструментах разработчика Chrome.

есть идеи, что я упускаю? Или текущий рабочий пример Python3 websocket?

1 ответов


когда я пытаюсь поговорить с вашим кодом python из Safari 6.0.1 на Lion, я получаю

Unexpected LF in Value at ...

в консоли Javascript. Я также получаю IndexError исключение из кода Python.

когда я говорю с вашим кодом python из Chrome версии 24.0.1290.1 dev на Lion, я не получаю никаких ошибок Javascript. В вашем javascript onopen() и onclose() вызываются методы, но не onmessage(). Код python не выдает никаких исключений и, похоже, имеет сообщение receive и отправил ответ, я.e именно такое поведение вы видите.

поскольку Safari не понравился трейлинг LF в вашем заголовке, я попытался удалить его, я.e

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

когда я делаю это изменение Chrome может видеть ваше ответное сообщение i.e

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 когда-либо огонь, и я все еще вижу IndexError исключение из python.

в заключение. Ваш код Python не работал с Chrome из-за дополнительного LF в вашем ответе заголовка. Там все еще что-то происходит, потому что код, который работает с Chrome, не работает с Safari.

обновление

Я разработал основную проблему и теперь имею пример, работающий в Safari и Chrome.

base64.encodestring() всегда добавляет трейлинг \n к нему вернемся. Это источник LF, на который жаловался Safari.

вызов .strip() о возврате стоимости calculate_websocket_hash и использование исходного шаблона заголовка работает правильно в Safari и Chrome.