Skip to content

How to create a new transport for Heral

Ahmad Shahwan edited this page Dec 9, 2015 · 20 revisions

Creating a new transport for Herald boils down to providing two services: herald.transport and herald.transport.directory. These services are usually implemented by two separate components.

Implementing herald.transport service

A component providing such a service must implement the following methods:

Method fire()

This method addresses a message to a given peer. It has the following signature:
fire(self, peer: herald.bean.Peer, message: heard.bean.Message, extra: object = None).

The method takes the peer object as a parameter, this object can be None in case of a replay to a newly discovered peer. It also takes the message to be send, this message must be serialised in a way to be sent by the underling protocol, see Serialisation below. An extra parameter is provided which holds, whenever relevant, information about the original message in case of a reply.

This method must resolve the address to whom the message is to be sent. This is done based on information encoded in extra, or extracted based on the peer object. For instance, in HTTP a peer's address is the set of its hostname, port number, and resource path.

In order for the destination address to be solvable, peer and extra cannot be both None.

A typical implementation of fire() can go as follows.

    def fire(self, peer, message, extra=None):
        address = extra['address'] if extra else self.__addresses[peer.uid]
        content = serialise(message, address=address)
        self.__send_to_address(address, content)

Method fire_group()

This method broadcasts a message to a given group. It has the following signature:

  • fire_group(self, group: str, peers: Set[herald.bean.Peer], message: heard.bean.Message) -> Set[herald.bean.Peer].

The method takes into parameter the group's name, it takes as well as set of peers belonging to that group. Depending on the implemented protocol, the method can either use broadcast, multicast, or publish utilities to deliver the message to the whole group at once, or iterate along the group's member, delivering the message to one at a time. The method returns the set of peers to whom the message was sent.

Like fire(), fire_group() must serialise message in a way that suits the underling transport protocol.

A typical implementation of fire_group() may look like this.

    def fire_group(self, group, peers, message):
        content = serialise(message, group=group)
        peers = self.__sent_to_group(group, content)
        return peers

Handling incoming messages

The transport layer should also handle incoming messages to allow Herald communications. To this end the transport client should either listen to a given port, pull messages regularly from a server, or subscribe to certain events in order to receive messages address to it or to one of its groups.

When a message is received, it is first unserialised to create a herald.beans.ReceivedMessage object. Once a message object is created, extra protocol-dependent properties can be loaded into the message, typically the extra property, which should contain information about the senders address. This is important during the discovery handshake where peers still don't know each others and have no means of guessing the reply address. The message is typically tagged by its transport method at this stage as well. This is done by means of an access ID.

Finally, and depending on the message subject, this method routes the message either to a PeerContact object, responsible of a 3-phase handshake protocol, or to the Herald core service.

Typically, a transport component should have a message-received call back that looks like follows.

    def on_message_received(self, content):
        message = unserialise(content)
        sender = message.get_header(herald.MESSAGE_HEADER_SENDER_UID)
        subject = message.subject

        message.set_extra(sender)
        message.set_access(ACCESS_ID)

        if subject.startswith(SUBJECT_DISCOVERY_PREFIX):
            self.__contact.herald_message(self._herald, message)
        else:
            self._herald.handle_message(message)

Serialisation

Herald uses JSON to serialise messages (message meta-data, headers and content). Serialisation is typically done by calling method herald.utils.to_json() on a herald.beans.Message object, after certain headers are set (such as sender UID). Deserialising a JSON-encoded message is done by calling herald.utils.from_json() on the string object.

Service properties

Access ID

A unique ID identifying the underling protocol.

  • Property key: herald.access.id.
  • Property type: String.

Protocol specific properties

The service may define more protocol specific properties, such as port, host name, or quality of services provided.

Service herald.transport.directory

Such a service must implement the following methods:

Method load_access()

Method has the following signature:
load_access(self, data: object) -> object.

It unserializes an object that contains enough information to locate a peer using the underling protocol. For instance, for HTTP such information is the host address, port number, and path.

Method peer_access_set()

Method has the following signature:
peer_access_set(self, peer: herald.bean.Peer, data: object).

This is a callback, invoked by Herald whenever a peer's access is set. data is the new access object.

Method peer_access_unset()

Method has the following signature:
peer_access_unset(self, peer: herald.bean.Peer, data: object).

This is a callback, invoked by Herald whenever a peer's access is unset.