How to Implement WebSocket Server Using Twisted
HTTP is a request-response type one way protocol. For web applications where continuous data needs to be sent, WebSocket was introduced. Unlike HTTP, WebSocket provides full duplex communication. WebSocket, which can be said as an upgraded version of HTTP, is standardized to be used over TCP like HTTP.
In this article I will share my experience in implementing WebSocket with Twisted, a framework of Python for internet. If you are familiar with WebSocket, then you can skip to the Twisted.web section.
WebSocket
To initiate communication using WebSocket, a handshake needs to be done between client and server. This procedure is backward compatible to HTTP's request-response structure. First the client sends a handshake request to the server:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13Sending Upgrade header in request with value websocket will acknowledge server about WebSocket communication. Now if server supports WebSocket with specified sub-protocols and version, it will send adequate response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chatIn response, server will send 101 Switching Protocols code and Sec-WebSocket-Accept whose value is calculated using Sec-WebSocket-Key. After a successful handshake, any of the peers can send data to each other which must be encoded in binary format described in WebSocket RFC.
WebSocket Frame Structure
A high-level overview of the framing:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+Twisted.web Implementation
As in normal twisted.web server, at TCP level, we have HTTPChannel class (a child class of protocol.Protocol) and server.Site class (which is the child class of protocol.ServerFactory). Also a Resource instance needs to be passed to server.Site class, so that it can serve GET requests.
Whenever data is received, dataReceived method of HTTPChannel is invoked. If data starts with 'GET', allow the HTTPChannel to handle it, which will invoke the render function of the root resource provided to Site class.
Echo WebSocket Server Example
1import base64, hashlib
2from twisted.internet import reactor
3from twisted.web.server import Site, http, resource
4
5class EchoResource(resource.Resource):
6 isLeaf = 1
7
8 def render(self, request):
9 # Processing the Key as per RFC 6455
10 key = request.getHeader('Sec-WebSocket-Key')
11 h = hashlib.sha1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
12
13 request.setHeader('Sec-WebSocket-Accept', base64.b64encode(h.digest()))
14
15 # Setting response headers
16 request.setHeader('Upgrade', 'websocket')
17 request.setHeader('Connection', 'Upgrade')
18 request.setResponseCode(101)
19
20 return ''
21
22class EchoChannel(http.HTTPChannel):
23 def dataReceived(self, data):
24 if data.startswith('GET'):
25 # This will invoke the render method of resource
26 http.HTTPChannel.dataReceived(self, data)
27 else:
28 # Decoding data using Frame module
29 f = frame.Frame(bytearray(data))
30 received_message = f.message()
31 print(received_message)
32
33 # Sending back the received message
34 msg = frame.Frame.buildMessage(received_message, mask=False)
35 self.transport.write(str(msg))
36
37class EchoSite(Site):
38 def buildProtocol(self, addr):
39 channel = EchoChannel()
40 channel.requestFactory = self.requestFactory
41 channel.site = self
42 return channel
43
44site = EchoSite(EchoResource())
45
46if __name__ == '__main__':
47 reactor.listenTCP(8080, site)
48 reactor.run()This implementation creates a simple echo server that upgrades HTTP connections to WebSocket and echoes back any messages it receives. The key components are:
- EchoResource: Handles the WebSocket handshake
- EchoChannel: Processes incoming WebSocket frames
- EchoSite: Factory for creating channel instances
Data sent to the client by server should be unmasked as per the WebSocket specification. The Frame.py module handles the binary framing according to the WebSocket RFC, making it simple to encode and decode messages.
Need Help With Your Project?
We've delivered production-ready solutions for startups and enterprises. Let's discuss your project.
Get in Touch