-
-
Save mumrah/512987 to your computer and use it in GitHub Desktop.
import time | |
import struct | |
import socket | |
import hashlib | |
import sys | |
from select import select | |
import re | |
import logging | |
from threading import Thread | |
import signal | |
class WebSocket(object): | |
handshake = ( | |
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" | |
"Upgrade: WebSocket\r\n" | |
"Connection: Upgrade\r\n" | |
"WebSocket-Origin: %(origin)s\r\n" | |
"WebSocket-Location: ws://%(bind)s:%(port)s/\r\n" | |
"Sec-Websocket-Origin: %(origin)s\r\n" | |
"Sec-Websocket-Location: ws://%(bind)s:%(port)s/\r\n" | |
"\r\n" | |
) | |
def __init__(self, client, server): | |
self.client = client | |
self.server = server | |
self.handshaken = False | |
self.header = "" | |
self.data = "" | |
def feed(self, data): | |
if not self.handshaken: | |
self.header += data | |
if self.header.find('\r\n\r\n') != -1: | |
parts = self.header.split('\r\n\r\n', 1) | |
self.header = parts[0] | |
if self.dohandshake(self.header, parts[1]): | |
logging.info("Handshake successful") | |
self.handshaken = True | |
else: | |
self.data += data | |
msgs = self.data.split('\xff') | |
self.data = msgs.pop() | |
for msg in msgs: | |
if msg[0] == '\x00': | |
self.onmessage(msg[1:]) | |
def dohandshake(self, header, key=None): | |
logging.debug("Begin handshake: %s" % header) | |
digitRe = re.compile(r'[^0-9]') | |
spacesRe = re.compile(r'\s') | |
part_1 = part_2 = origin = None | |
for line in header.split('\r\n')[1:]: | |
name, value = line.split(': ', 1) | |
if name.lower() == "sec-websocket-key1": | |
key_number_1 = int(digitRe.sub('', value)) | |
spaces_1 = len(spacesRe.findall(value)) | |
if spaces_1 == 0: | |
return False | |
if key_number_1 % spaces_1 != 0: | |
return False | |
part_1 = key_number_1 / spaces_1 | |
elif name.lower() == "sec-websocket-key2": | |
key_number_2 = int(digitRe.sub('', value)) | |
spaces_2 = len(spacesRe.findall(value)) | |
if spaces_2 == 0: | |
return False | |
if key_number_2 % spaces_2 != 0: | |
return False | |
part_2 = key_number_2 / spaces_2 | |
elif name.lower() == "origin": | |
origin = value | |
if part_1 and part_2: | |
logging.debug("Using challenge + response") | |
challenge = struct.pack('!I', part_1) + struct.pack('!I', part_2) + key | |
response = hashlib.md5(challenge).digest() | |
handshake = WebSocket.handshake % { | |
'origin': origin, | |
'port': self.server.port, | |
'bind': self.server.bind | |
} | |
handshake += response | |
else: | |
logging.warning("Not using challenge + response") | |
handshake = WebSocket.handshake % { | |
'origin': origin, | |
'port': self.server.port, | |
'bind': self.server.bind | |
} | |
logging.debug("Sending handshake %s" % handshake) | |
self.client.send(handshake) | |
return True | |
def onmessage(self, data): | |
logging.info("Got message: %s" % data) | |
def send(self, data): | |
logging.info("Sent message: %s" % data) | |
self.client.send("\x00%s\xff" % data) | |
def close(self): | |
self.client.close() | |
class WebSocketServer(object): | |
def __init__(self, bind, port, cls): | |
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
self.socket.bind((bind, port)) | |
self.bind = bind | |
self.port = port | |
self.cls = cls | |
self.connections = {} | |
self.listeners = [self.socket] | |
def listen(self, backlog=5): | |
self.socket.listen(backlog) | |
logging.info("Listening on %s" % self.port) | |
self.running = True | |
while self.running: | |
rList, wList, xList = select(self.listeners, [], self.listeners, 1) | |
for ready in rList: | |
if ready == self.socket: | |
logging.debug("New client connection") | |
client, address = self.socket.accept() | |
fileno = client.fileno() | |
self.listeners.append(fileno) | |
self.connections[fileno] = self.cls(client, self) | |
else: | |
logging.debug("Client ready for reading %s" % ready) | |
client = self.connections[ready].client | |
data = client.recv(1024) | |
fileno = client.fileno() | |
if data: | |
self.connections[fileno].feed(data) | |
else: | |
logging.debug("Closing client %s" % ready) | |
self.connections[fileno].close() | |
del self.connections[fileno] | |
self.listeners.remove(ready) | |
for failed in xList: | |
if failed == self.socket: | |
logging.error("Socket broke") | |
for fileno, conn in self.connections: | |
conn.close() | |
self.running = False | |
if __name__ == "__main__": | |
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s") | |
server = WebSocketServer("localhost", 9999, WebSocket) | |
server_thread = Thread(target=server.listen, args=[5]) | |
server_thread.start() | |
# Add SIGINT handler for killing the threads | |
def signal_handler(signal, frame): | |
logging.info("Caught Ctrl+C, shutting down...") | |
server.running = False | |
sys.exit() | |
signal.signal(signal.SIGINT, signal_handler) | |
while True: | |
time.sleep(100) |
If the SIGINT trap doesn't work, you could try using KeyboardInterrupt
http://docs.python.org/library/exceptions.html#exceptions.KeyboardInterrupt
The SIGINT trap itself works but only if I keep the main thread alive with the while-loop. The same goes for the KeyboordInterrupt which I only seem to catch like this:
while 1:
try:
time.sleep(100)
except KeyboardInterrupt:
logging.info("KeyboardInterrupt, shutting down...")
server.running = False
sys.exit()
I'm on a Mac. Maybe these things vary per platform?... Anyway, I've had a lot of fun playing with your code. Thx again for making this available.
Another issue I encountered was the 'Address already in use' error I got when quitting and immediately restarting the server. I fixed it by inserting this code just before line 99
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
i found that line 80 occasionally fail, and i had to delay "adding the md5 response to the handshake (line 76)", to after the string substitution.
Under Python 3 'str' and 'bytes' are handled differently during transmission...strings must be converted to bytes for transmission and bytes must be converted to strings on receive
Line 39:
Original ---> self.header += data
Error ---> Can't convert 'bytes' object to str implicitly
Modification ---> self.header += bytes.decode(data, encoding='utf_8', errors='strict')
Line 83:
original ---> self.client.send(handshake)
error ---> TypeError: 'str' does not support the buffer interface
modification ---> self.client.send(str.encode(handshake, encoding='utf_8', errors='strict'))
With those modifications the websockets servery is working on python 3.2
As gitdlam pointed out, line 80 occasionally fails, which leads to self.client.send(handshake) never getting called.
This occurs when the challenge response contains a %, which messes up the string substitution.
A solution is to comment out line 80, and
-
change line 76 to:
handshake = (WebSocket.handshake % {'origin': origin, 'port': self.server.port, 'address': self.server.address}) + response -
line 79 to:
handshake = (WebSocket.handshake % {'origin': origin, 'port': self.server.port, 'address': self.server.address})
@danielfaust, @remcoder, thanks updated gist
Hi. Great work. What version of the protocol currently used?
hixie-76 (hybi-00) . As of Oct. 2011 only Opera and Safari support this old version. Doesn't work with Chrome and Firefox anymore :(
You might want to check out Tavendo Autobahn or pywebsocket as an alternative (hybi-10). Though I'd really like to see this server getting upgraded sometime.
This was really just a prototype to see WebSockets working with Python. For an up-to-date (and more complete) implementation check out Tornado https://github.com/facebook/tornado/blob/master/tornado/websocket.py
Thanks. Tornado seems is that I need :)
I spent literally hours looking around for a really simple WebSocket implemention in Python over the past couple of days. There are lots of frameworks and helpers, though I wanted something that was self contained and didn't have a list of dependencies as long as my arm. In the end I adapted this so that it works with the RFC version of WebSockets. It's slightly hacky at the moment, though hopefully will help somebody. See here for the code: https://gist.github.com/4190745
Sorry, the URL above should have been https://gist.github.com/4190781
How would you close a connection from client correctly
This piece of code saved Zeokat lots of hours. Deal with HTTP protocol is always painfull and the rfc2616 grgrgr
also, CTRL-C didn't work. I added these lines at the very end:
while 1:
time.sleep(100)
and don't forget to
import time