Skip to content

Instantly share code, notes, and snippets.

@ceronman
Created October 5, 2012 00:03
Show Gist options
  • Select an option

  • Save ceronman/3837236 to your computer and use it in GitHub Desktop.

Select an option

Save ceronman/3837236 to your computer and use it in GitHub Desktop.
Code I'm proud of
# In Pixtream, a P2P program I wrote a few years ago, I used a custom binary
# protocol similar to the one used by BitTorrent. The peers in Pixtream
# communicate with each other using small binary messages sent through TCP.
# Each message started with four bytes indicating the message's length, and a
# single byte indicating the message's type. The protocol consisted of several
# different messages types.
# The Handshake message was something like this:
# [Message Length][Message Header][Protocol ID][Extensions][Peer ID]
# In bytes it would look like this:
# \x00\x00\x00.HPixtream Protocol0000000000000000000000000001
# Because I was designing the protocol at the same time that I was writing the
# program, the messages used to change all the time. I wanted a flexible way of
# specifying the messages format, how too parse it and how to validate them.
# I wrote a set of classes to write all the messages descriptions
# declaratively:
class Message(object):
"""Base class for messages.
Can be used to parse new messages and create appropriate objects.
"""
message_header = '?'
_header_map = {}
_prefix_struct = struct.Struct('>I')
@classmethod
def register(cls, message_class):
"""Registers a class message.
Decorator used to register the class message_header in a dictionary.
This way messages could be identified and objects of proper classes
will be created.
"""
assert issubclass(message_class, Message)
assert len(message_class.message_header) == 1
assert message_class.message_header not in cls._header_map
assert hasattr(message_class, 'fields')
message_class._parse_fields()
message_class._create_message_struct()
cls._header_map[message_class.message_header] = message_class
return message_class
@classmethod
def parse(cls, data):
"""Parse a byte string and creates a message object.
Parses a message byte string got from the network and creates a proper
Message object of the right subclass and _unpack the string in that
object.
"""
if len(data) == 0:
raise MessageException('Decoding empty message')
message_type = data[0]
message_class = cls._header_map.get(message_type, None)
if message_class is None:
raise MessageException('Got unregistered message', data)
message = message_class()
try:
message._unpack(data)
except struct.error:
raise MessageException('Message is not well formatted', data)
return message
def pack(self):
"""Pack the message object into a byte string.
Packs the message into a byte string to be send through the network.
"""
if not self.is_valid():
raise MessageException("Trying to pack an invalid message")
values = [getattr(self, name) for name in self.field_names]
return self._message_struct.pack(*values)
def pack_prefixed(self):
"""Pack the message with a prefix.
Works as pack but returns the message with a four bytes integer
representing the size of the message.
"""
message = self.pack()
return self._prefix_struct.pack(len(message)) + message
def is_valid(self):
"""Returns True if the data in the message is valid."""
try:
return (self.message_header == self.__class__.message_header and
all(self.valid_conditions()))
except:
return False
def valid_conditions(self):
"""Generators that yields conditions necessary for the message to be
valid.
Subclasses should override this method.
"""
return
yield
@classmethod
def create(cls):
"""Create a new packet.
Use this instead of the default constructor to build a new message
object. Subclasses should override this method.
"""
return cls()
def _unpack(self, data):
fields = self._message_struct.unpack(data)
for i, name in enumerate(self.field_names):
setattr(self, name, fields[i])
@classmethod
def _parse_fields(cls):
cls.field_names = ['message_header']
cls.field_structs = ['s'] # struct string for message_header
for field in cls.fields:
assert isinstance(field, Field)
cls.field_names.append(field.name)
cls.field_structs.append(field.struct_string)
@classmethod
def _create_message_struct(cls):
struct_string = '>' # big endian
struct_string += ''.join(cls.field_structs)
cls._message_struct = struct.Struct(struct_string)
# The way of defining a message in the protocol was to subclass the Message
# class and register it with the parsing system. The class should contain the
# message format with specific bytes and lengths and also the set of conditions
# needed for the message to be valid. It also has the code to to create a
# message based on some provided parameters. For example, here is the
# definition of the Handshake message:
@Message.register
class HandshakeMessage(Message):
"""The first message send when two peers connect with each other.
"""
message_header = 'H'
fields = [
Field('17s', 'protocol_id',
"""'Pixtream Protocol' string"""),
Field('8s', 'extensions',
"""Reserved for future extensions"""),
Field('20s', 'peer_id',
"""Unique ID of the peer"""),
]
def valid_conditions(self):
yield self.protocol_id == 'Pixtream Protocol'
yield len(self.peer_id) == 20
@classmethod
def create(cls, peer_id):
assert len(peer_id) == 20
msg = cls()
msg.peer_id = peer_id
msg.protocol_id = 'Pixtream Protocol'
msg.extensions = '00000000'
return msg
# In the application code, a message could be easily created:
handshake = HandshakeMessage.create(peer_id)
# To send a message, I just had to pack the object to get the binary
# representation for that message:
self.transport.write(handshake.pack_prefixed())
# When a message was received, it could be decoded and parsed like this:
message = Message.parse(binary_message)
if isinstance(message, HandshakeMessage):
sendHandshake(message.peer_id)
# I'm proud of this code because of three reasons:
# 1. It made really easy to modify the protocol on the fly and adjust the
# messages at the same time that the program was evolving.
# 2. It clearly separated the message definition and validation from the
# communication logic.
# 3. Because the protocol was described in a declarative way, I was able to
# write a simple program that generated Latex documentation of the protocol
# specs. Using the docstrings of the fields and messages.
# This is a simplified version of the code, Pixtream contained some more
# complex messages, the full code for the Message definition can be found here:
# https://github.com/ceronman/pixtream/blob/master/src/pixtream/peer/messages.py
# And all the messages defined can be found here:
# https://github.com/ceronman/pixtream/blob/master/src/pixtream/peer/specs.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment