Skip to content

Instantly share code, notes, and snippets.

@mumrah
Created August 7, 2010 17:01
Show Gist options
  • Save mumrah/512987 to your computer and use it in GitHub Desktop.
Save mumrah/512987 to your computer and use it in GitHub Desktop.
Simple WebSockets in Python
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)
@dmytty
Copy link

dmytty commented Mar 4, 2011

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

@danielfaust
Copy link

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

  1. change line 76 to:
    handshake = (WebSocket.handshake % {'origin': origin, 'port': self.server.port, 'address': self.server.address}) + response

  2. line 79 to:
    handshake = (WebSocket.handshake % {'origin': origin, 'port': self.server.port, 'address': self.server.address})

@mumrah
Copy link
Author

mumrah commented Apr 29, 2011

@danielfaust, @remcoder, thanks updated gist

@gleb-chipiga
Copy link

Hi. Great work. What version of the protocol currently used?

@danielfaust
Copy link

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.

@mumrah
Copy link
Author

mumrah commented Oct 11, 2011

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

@gleb-chipiga
Copy link

Thanks. Tornado seems is that I need :)

@rich20bb
Copy link

rich20bb commented Dec 2, 2012

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

@rich20bb
Copy link

rich20bb commented Dec 2, 2012

Sorry, the URL above should have been https://gist.github.com/4190781

@sloflash
Copy link

How would you close a connection from client correctly

@Zeokat
Copy link

Zeokat commented Mar 5, 2014

This piece of code saved Zeokat lots of hours. Deal with HTTP protocol is always painfull and the rfc2616 grgrgr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment