2012-10-14 17 views
7

Próba utworzenia strony WWW dla aplikacji wspieranej przez Python3. Aplikacja będzie wymagać dwukierunkowego przesyłania strumieniowego, co brzmi jak dobra okazja do zaglądania do stron internetowych.Implementacja Websocket w Pythonie 3

Moja pierwsza skłonność polegała na używaniu czegoś już istniejącego, a przykładowe aplikacje z mod-pywebsocket okazały się cenne. Niestety ich interfejs API nie wydaje się łatwo poddać rozszerzeniu, a jest to Python2.

Rozglądając się po blogosferze wiele osób napisało własny serwer websocket dla wcześniejszych wersji protokołu websocket, większość nie implementuje skrótu klucza bezpieczeństwa, więc nie działa.

Reading RFC 6455 postanowiłem wziąć ukłucie w nim siebie i wyszedł z następujących czynności:

#!/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() 

Stosując tę ​​podstawową stronę testową (która współpracuje z 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> 

Odbiera dane i demaskuje je poprawnie, ale nie mogę uzyskać ścieżki transmisji do działania.

Jako test napisać „cześć” do gniazda, program powyżej oblicza bajtów do zapisu do gniazda jak:

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

które odpowiadają wartości podanych w section 5.7 sześciokątne z RFC. Niestety ramka nigdy nie pojawia się w Narzędziach dla programistów Chrome.

Masz pomysł, czego mi brakuje? Lub aktualnie działający przykład websocket Python3?

+0

Tornado obsługuje zarówno websockets, jak i Python 3. http://www.tornadoweb.org/documentation/websocket.html –

+0

Dzięki Thomas. Najpierw chciałbym jednak mieć samodzielną implementację - chodzi o zrozumienie protokołu jako rozwiązania problemu. Spojrzenie na [kod źródłowy tornada] (https://github.com/facebook/tornado/blob/master/tornado/websocket.py) Widzę jeden nagłówek ** Sec-WebSocket-Protocol ** wysyłany z serwer do klienta, ale [spec] (http://tools.ietf.org/html/rfc6455#section-4.2.2) mówi, że jest opcjonalne. – Hardbyte

+0

Jeśli klient zażąda pod-protokołu, serwer powinien go wyświetlić (zawsze zakładając, że obsługuje pod-protokół). Niewykonanie tej czynności może spowodować błąd uzgadniania, więc prawdopodobnie nie jest to związane z problemami z wysyłaniem wiadomości. – simonc

Odpowiedz

7

gdy próbuję mówić do kodu Pythona z Safari 6.0.1 na Lion uzyskać

Unexpected LF in Value at ... 

w konsoli JavaScript. Dostaję również wyjątek IndexError z kodu Python.

Kiedy rozmawiam z kodem Pythona z Chrome w wersji 24.0.1290.1 ​​na Lion, nie otrzymuję żadnych błędów Javascript. W twoim javascriptu wywoływane są metody onopen() i onclose(), ale nie onmessage(). Kod Pythona nie generuje żadnych wyjątków i wydaje się, że otrzymał komunikat i wysłał odpowiedź, tj. Dokładnie to, co widzisz.

Ponieważ Safari nie podoba LF spływu w nagłówku Próbowałem usunięcie go, tj

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

Kiedy dokonać tej zmiany Chrome jest w stanie zobaczyć komunikat odpowiedzi tj

got: Hello 

pojawia się w konsoli javascript.

Safari nadal nie działa. Teraz podnieście to inna kwestia, gdy próbuję wysłać wiadomość.

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. 

Żaden z javascript obsługi zdarzeń websocket nigdy ogień, a ja wciąż widząc IndexError wyjątek od pytona.

Podsumowując. Twój kod Pythona nie działał w Chrome z powodu dodatkowego LF w odpowiedzi na nagłówek. Nadal dzieje się coś jeszcze, ponieważ kod działający z Chrome nie działa z Safari.

Aktualizacja

Pracowałem już z podstawowego problemu, a teraz mamy przykład pracuje w Safari i Chrome.

base64.encodestring() dodaje zawsze końcowy \n do swojego powrotu. To jest źródło LF, na które uskarża się Safari.

zadzwoń pod numer .strip() po zwróceniu wartości calculate_websocket_hash, a korzystanie z oryginalnego szablonu nagłówka działa poprawnie w Safari i Chrome.

+0

Awesome, after usunięcie tej dodatkowej CRLF działa teraz w Firefoksie i Chrome. – Hardbyte