Created
October 5, 2012 00:03
-
-
Save ceronman/3837236 to your computer and use it in GitHub Desktop.
Code I'm proud of
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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