2012-10-14 23 views
7

Tentativo di creare un front-end Web per un'applicazione con supporto Python3. L'applicazione richiederà uno streaming bidirezionale che sembra una buona opportunità per esaminare le web socket.Implementazione Websocket in Python 3

La mia prima inclinazione era quella di utilizzare qualcosa già esistente, e le applicazioni di esempio da mod-pywebsocket si sono rivelate preziose. Sfortunatamente la loro API non sembra prestarsi facilmente all'estensione, ed è Python2.

Guardando intorno alla blogosfera molte persone hanno scritto il proprio server websocket per le versioni precedenti del protocollo websocket, la maggior parte non implementa l'hash della chiave di sicurezza quindi non funziona.

lettura RFC 6455 ho deciso di prendere una pugnalata a me stesso e si avvicinò con la seguente:

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

Tramite questa pagina di base di test (che funziona con 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> 

Questo riceve i dati e li demase correttamente, ma non riesco a far funzionare il percorso di trasmissione.

Come test per scrivere "Ciao" alla presa, il programma calcola sopra il byte da scrivere alla presa come:

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

corrispondenti valori esadecimali indicati nella section 5.7 del RFC. Sfortunatamente la cornice non viene mai visualizzata negli Strumenti per sviluppatori di Chrome.

Qualche idea di cosa mi manca? O un esempio di websocket Python3 attualmente funzionante?

+0

Tornado supporta sia websocket sia Python 3. http://www.tornadoweb.org/documentation/websocket.html –

+0

Grazie Thomas. Mi piacerebbe avere una implementazione standalone prima però - questo è tanto per capire il protocollo che per risolvere un problema per me. Dando un'occhiata al [codice sorgente di Tornado] (https://github.com/facebook/tornado/blob/master/tornado/websocket.py) Vedo un'intestazione ** Sec-WebSocket-Protocol ** inviata dal server al client, ma la [specifica] (http://tools.ietf.org/html/rfc6455#section-4.2.2) dice che è opzionale. – Hardbyte

+0

Se un client richiede un sottoprotocollo, è previsto che il server lo faccia eco (sempre supponendo che supporti il ​​sottoprotocollo). In caso contrario, si verificherà un errore di handshake, pertanto probabilmente non si tratta di problemi di invio del messaggio. – simonc

risposta

7

Quando provo a parlare con il vostro codice python da Safari 6.0.1 su Lion ottengo

Unexpected LF in Value at ... 

nella console Javascript. Ricevo anche un'eccezione IndexError dal codice Python.

Quando parlo con il codice Python da Chrome versione 24.0.1290.1 ​​su Lion non ho errori Javascript. Nel tuo javascript sono chiamati i metodi onopen() e onclose(), ma non lo onmessage(). Il codice python non genera eccezioni e sembra che abbia ricevuto un messaggio e abbia inviato la sua risposta, cioè il comportamento che stai vedendo.

Dal momento che Safari non piaceva l'LF finale nell'intestazione Ho provato a rimuoverlo, cioè

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

Quando faccio questo cambiamento Chrome è in grado di vedere il vostro messaggio di risposta cioè

got: Hello 

appare nella console javascript.

Safari continua a non funzionare. Ora sollevare un altro problema quando tento di inviare un messaggio.

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. 

Nessuno dei gestori di eventi websocket javascript mai fuoco e sto ancora a vedere l'eccezione IndexError in pitone.

In conclusione. Il tuo codice Python non funzionava con Chrome a causa di un LF extra nella risposta dell'header. C'è ancora qualcos'altro che sta succedendo perché il codice che funziona con Chrome non funziona con Safari.

Aggiornamento

ho lavorato fuori la questione di fondo e ora hanno l'esempio di lavoro in Safari e Chrome.

base64.encodestring() aggiunge sempre un trailing \n al suo ritorno. Questa è la fonte della LF di cui Safari si lamentava.

chiamare .strip() sul valore restituito di calculate_websocket_hash e utilizzare il modello di intestazione originale funziona correttamente su Safari e Chrome.

+0

Impressionante, dopo rimuovendo quel CRLF in più ora funziona per Firefox e Chrome. – Hardbyte