Tornado TCP Server tutorial (Part III)

TL;DR
From this post you’ll learn how to create a simple protocol for TCP communication between the client and the server. All code is on GitHub.

Roadmap

  1. In the first post we’ve learned how to build simple echo TCP Server in Tornado.
  2. In the second post we connected Tornado to Redis via Pub/Sub mechanism in order to deliver data updates in real time.
  3. This final part will cover the implementation of the simple protocol that can control client-server interaction.

Subscription Protocol

So, we’ve attached Redis instance to our Tornado TCP server and learned how to pass the Pub/Sub messages to the clients. The next logical step would be to implement a simple protocol that can control adding and removing clients from available update channels.

For example, you might have several update channels for different sporting events you’re covering in your feed. Any user can subscribe/unsubscribe to any of these events.

Any protocol is just a set of rules of communication between parties. Be it HTTP, TCP, IP, RPC, you call it. And we’ll make our own now. Yay! So let’s get started.

First, let’s lay out the commands we’ll need for our communication. We’ll keep it simple with just 2 commands:

  • SUBSCRIBE: tells the server to subscribe the client to the default channel.
  • UNSUBSCRIBE: the reverse action, removing client from the list of those receiving updates from the channel.

ClientConnection class will change a little bit, with addition of one new method for handling this logic:

def _handle_request(self, request):
if request == 'SUBSCRIBE':
if not self._subscribed:
self._subscribed = True
return 'CONFIRMED'
else:
return 'ALREADY SUBSCRIBED'
elif request == 'UNSUBSCRIBE':
if not self._subscribed:
return 'ALREADY UNSUBSCRIBED'
else:
self._subscribed = False
return 'CONFIRMED'
else:
return 'UNKNOWN COMMAND'
view raw server.py hosted with ❤ by GitHub

As you can see here, the server has 3 kinds of responses:

  • CONFIRMED — subscribe/unsubscribe request was successful.
  • ALREADY SUBSCRIBED — same subscription request was received from the client.
  • UNKNOWN COMMAND — for all the commands that aren’t supported.

Where should we put the handler call in our class? Well, we have our rather useless run routine that just echoes client’s messages back. Let’s tweak its logic just a little bit so it can handle protocol commands. Note the highlighted line:

@gen.coroutine
def run(self):
while True:
try:
request = yield self._stream.read_until(
self.message_separator)
request_body = request.rstrip(self.message_separator)
request_body_str = request_body.decode('utf-8')
except StreamClosedError:
self._stream.close(exc_info=True)
return
else:
response_body = self._handle_request(request_body_str)
response_body_bytes = response_body.encode('utf-8')
response = response_body_bytes + self.message_separator
try:
yield self._stream.write(response)
except StreamClosedError:
self._stream.close(exc_info=True)
return
view raw server.py hosted with ❤ by GitHub

Finally, let’s run our code. As previously, open the Terminal and connect to the Redis instance with redis-cli. Then, start the server:

$ python real_time/server.py
Starting the server...
Subscribed to "updates" Redis channel.

Start the client:

$ python real_time/client.py
Connecting to the server socket...

Publish something in the Redis channel:

127.0.0.1:6379> PUBLISH updates "this is only for subscribers ;)"

We see nothing in the client console:

$ python real_time/client.py
Connecting to the server socket...

So let’s issue subscribe command:

$ python real_time/client.py
Connecting to the server socket...
SUBSCRIBE
b'CONFIRMED'

Now publish something in Redis again:

127.0.0.1:6379> PUBLISH updates "this is again only for subscribers ;)"

And, voila, the client got the message:

$ python real_time/client.py
Connecting to the server socket...
SUBSCRIBE
b'CONFIRMED'
b'this is again only for subscribers ;)'

You can check yourself the following use cases:

  1. After unsubscribing no more messages will be delivered to this client.
  2. In case of several clients system correctly sends the messages only to those who are subscribed.
  3. Any unknown commands are ignored.

The final code for the project is in on GitHub. There’s also a Python 3.4 compatible version in python-3.4 branch.

You can notice some slight differences on GitHub with the code I’ve brought here. Those are primarily technical adjustments meant to smooth the process of stopping the server and client scripts. The code in repo is fully functional so give it a try!

As the Homework task, I suggest you extend our protocol and add the ability to choose the channel to subscribe to.

Conclusion

That’s it for this post and the whole Real Time series, guys. I hope you liked it and found something useful. Please comment if you have any questions and/or ideas. Cheers and don’t brawl, stay calm.

References:

  1. stackoverflow.com: How to build a protocol on top of tcp?
  2. quora.com: How do you write your own protocol that sits on top of TCP/IP?
  3. drdobbs.com: Designing a Network Protocol

Serge Mosin

https://www.databrawl.com/author/svmosingmail-com/

Pythonista, Data/Code lover, Apline skier and gym hitter.