Skip to content

Instantly share code, notes, and snippets.

@mumrah
Created August 7, 2010 17:01
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 17 You must be signed in to fork a gist
  • 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)
@mumrah
Copy link
Author

mumrah commented Aug 7, 2010

Added a JSON-RPC example

@mgielda
Copy link

mgielda commented Oct 17, 2010

Hey! Great work, I got it to run without any major problems, but I had to:

import signal
import threading
change Myserver->WebSocket in line 138.

Otherwise it was perfect, unlike all those other implementations on the Web which can't seem to work.

Thanks!
M.

@mumrah
Copy link
Author

mumrah commented Oct 18, 2010

@thalain, thanks updated the gist

@remcoder
Copy link

I had to change line 141 into this to get it to work:
server_thread = threading.Thread(target=server.listen, args=[5])

Just like thalain said, this is the first working Python implementation I got to work (the other ones are outdated I think). Thanks for the great work!

@remcoder
Copy link

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

@mumrah
Copy link
Author

mumrah commented Oct 31, 2010

If the SIGINT trap doesn't work, you could try using KeyboardInterrupt

http://docs.python.org/library/exceptions.html#exceptions.KeyboardInterrupt

@remcoder
Copy link

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.

@remcoder
Copy link

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)

@gitdlam
Copy link

gitdlam commented Feb 26, 2011

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.

@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