Over the years I’ve developed a lot of tricks and techniques for building systems with plugins, remote peers, and multiple components. Recently while working on a new project I decided to pull all of those tricks together into a single package. What came out is “channels” which is both a protocol description and a design pattern. Implementing “channels” leads to systems that are robust, easy to develop, debug, test, and maintain.
The channels protocol borrows from XML syntax to provide a means for processes to communicate structured data in multiplexed channels over a single bi-directional connection. This is particularly useful when the processes are engaged in multiple simultaneous transactions. It has the added benefit of simplifying logging, debugging, and development because segments of the data exchanged between processes can be easily packaged into XML documents where they can be transformed with XSLT, stored in databases, and otherwise parsed and analyzed easily.
With a little bit of care it’s not hard to design inter-process dialects that are also easily understood by people (not just machines), readily searched and parsed by ordinary text tools like grep, and easy to interact with using ordinary command line tools and terminal programs.
Peer: A process running on one end of a connection.
Connection: A bi-directional stream. Usually a pipe to a child process or TCP connection.
Line: The basic unit of communication for channels. A line of utf-8 text ending in a new-line character.
Message: One or more lines of text encapsulated in a recognized XML tag.
Channel Marker: A unique ch attribute shared among messages in a channel.
Channel: One or more messages associated with a particular channel marker.
Chatter: Text that is outside of a channel or message.
How to “do” channels…
First, open a bi-directional stream. That can be a pipe, or a TCP socket or some other similar mechanism. All data will be sent in utf-8. The basic unit of communication will be a line of utf-8 text terminated with a new-line character.
Since the channels protocol borrows from XML we will also use XML encoding to keep things sane. This means that some characters with special meanings, such as angle brackets, must be encoded when they are to be interpreted as that character.
Each peer can open a channel by sending a complete XML tag with an optional channel marker (@ch) and some content. The tag acts as the root element of all messages on a given channel. The name of the tag specifies the “dialect” which describes the language that will be used in the channel.
The unique channel marker describes the channel itself so that all of the messages associated with the channel can be considered part of a single conversation.
Channels cannot change their dialect, channel markers must be unique within a a connection, and channel markers cannot be reused. This allows each channel marker to act as a handle that uniquely identifies any given channel.
A good design will take advantage of the uniqueness of channel markers to simplify debugging and improve the functionality of log files. For example, it might be useful in some applications to use structured channel markers that convey some additional meaning and to make the markers unique across multiple systems and longer periods of time. It is usuall a good idea to include a serialized component so that logged channels can be easily sorted. Another tip would be to include something that is unique about a particular peer so that channels that are initiated at one end of the pipe can be easily distinguished from those initiated at the other end. (such as a master-slave relationship). All of that said, it is perfectly acceptable to use simple serialized channel markers where appropriate.
Messages are always sent atomically. This allows multiple channels to be open simultaneously.
An example of opening a channel might look like this:
<dialect ch='0'>This message is opening this channel.</dialect>
Typically each channel will represent a single “conversation” concerning a single transaction or interaction. The conversation is composed of one or more messages. Each message is composed of one or more lines of text and is bound by opening and closing tags with the same dialect.
<dialect ch='0'>This is another message in the same channel.</dialect> <dialect ch='0'> As long as the ch is the same, each message is part of one conversation. This allows multiple conversations to take place over a single connection simultaneously - even in different dialects. Multi-line messages are also ok. The last line ends with the appropriate end tag. </dialect>
By the way, indenting is not required… It’s just there to make things easier to read. Good design include a little extra effort to make things human friendly 😉
<dialect ch='0'> Either peer can send a message within a channel and at the end, either peer can close the channel by sending a closed tag. Unlike XML, closed tags have a different meaning than empty elements. In the channels protocol a closed tag represents a NULL while an empty element represents an empty message. The next message is simply empty. </dialect> <dialect ch='0'></dialect> <dialect ch='0'> The empty message has no particular meaning but might be used to keep a channel open whenever a timeout is being used to verify that the channel is still working properly. When everything has been said about a particular interaction, such as when a transaction is completed, then either peer can close the channel. Normally the peer to close the channel is the one that must be satisfied before the conversation is over. I'll do that next. </dialect> <dialect ch='0'/> The channel '0' is now closed.
Note that additional text after a closing tag is considered to be chatter and will often be ignored. However since it is part of a line that is part of the preceding message it will generally be logged with that message. This makes this kind of text useful for comments like the one above. Remember: the basic unit of communication in the channels protocol is a line of utf-8 text. The extra chatter gets included with the channel close directive because it’s on the same line.
Any new message must be started on a different line because the opening tag of any message must be the first non-whitespace on a line. That ensures that messages and related chatter are never mixed together at the line level.
Lines that are not encapsulated in XML tags or are not recognized as part of a known dialect are called chatter.
Chatter can be used for connection setup, keep-alive messages, readability, connection maintenance, or as diagnostic information. For example, a child process that uses the channels protocol might send some chatter when it starts up in order to show it’s version information and otherwise identify itself.
Allowing chatter in the protocol also provides a graceful failure mechanism when one peer doesn’t understand the messages from the other. This happens sometimes when the peers are using different software versions. Logging all chatter makes it easier to debug these kinds of problems.
Chatter can also be useful for facilitating debugging functions out-of-band making software development easier because the developer can exercise a peer using a terminal or command line interface. Good design practice for systems that implement the channels protocol is for the applications to use human-friendly syntax in the design of their dialects and to provide useful chatter especially when operating in a debug mode. That said, chatter is not required, but systems implementing the channels protocol must accept chatter gracefully.
A bit of a summary: The channels protocol is built up in layers. The first layer is a bi-directional connection. On top of that is chatter in the form of utf-8 lines of text and the use of XML encoding to escape some special characters.
- Layer 0: A bi-directional stream / connection between two peers.
- Layer 1: Lines of utf-8 text ending in a new-line character.
- Lines that are not recognized are chatter.
- Lines that are tagged and recognized are messages.
- Layer 2: There are two kinds of chatter:
- Noise. Unrecognized chatter should be logged.
- Diagnostics. Chatter recognized for low-level functions and testing.
- Layer 3: There are two kinds of messages:
- Shouts. A single message without a channel marker.
- Channels. One or more messages sharing a unique channel marker.
In addition to those low-level layers, each channel can contain other channels if this kind of multiplexing is supported by the dialect. The channels protocol can be applied recursively providing sub-channels within sub-channels.
- Layer …
- Layer n: The channels protocol is recursive. It is perfectly acceptable to implement another channels layer inside of a channel – or shouts within shouts. However, the peers need to understand the layering by treating the contents of a channel (or shout) as the connection for the next channels layer.
Consider a system where the peers have multiple sub-systems each serving serving multiple requests for other clients. Each sub-system might have it’s own dialect serving multiple simultaneous transactions. In addition there might be a separate dialect for management messages.
Now consider middle-ware that combines clusters of these systems and multiplexes them together to perform distributed processing, load balancing, or distributed query services. Each channel and sub-channel might be mapped transparently to appropriate layers over persistent connections within the cluster.
Channels protocol synopsis:
Uses a bi-directional stream like a pipe or TCP connection.
The basic unit of communication is a line of utf-8 text ending in a new-line.
Leading whitespace is ignored but preserved.
A group of lines bound by a recognized XML-style tag is a message.
When beginning a message the opening tag should be the first non-whitespace characters on the line.
When ending a message the ending tag should be the last non-whitespace characters on the line.
The name of the opening tag defines the dialect of the message. The dialect establishes how the message should be parsed – usually because it constrains the child elements that are allowed. Put another way, the dialect usually describes the root tag for a given XML schema and that specifies all of the acceptable subordinate tags.
The simplest kind of message is a shout. A shout always contains only a single message and has no channel marker. Shouts are best used for simple one-way messages such as status updates, simple commands, or log entries.
<dialect>This is a one line shout.</dialect> <shout>This shout uses a different dialect.</shout> <longer-shout> This is a multi-line shout. The indenting is only here to make things easier to read. Any valid utf-8 characters can go in a message as long as the message is well- formed XML and the characters are properly escaped. </longer-shout>
For conversations that require responses we use channels.
Channels consist of one or more messages that share a unique channel marker.
The first message with a given channel marker “opens” the channel.
Any peer can open a channel at any time.
The peer that opens a channel is said to be the initiator and the other peer is said to be the responder. This distinction is not enforced in any way but is tracked because it is important for establishing the role of each peer.
A conversation is said to occur in the order of the messages and typically follows a request-response format where the initiator of a channel will send some request and the responder will send a corresponding response. This will generally continue until the conversation is ended.
Example messages in a channel:
<dialect ch='0'> This is the start of a message. This line is also part of the message. The indenting makes things easier to read but isn't necessary. <another-tag> The message can contain additional XML but it doesn't have to. It could contain any string of valid utf-8 characters. If the message does contain XML then it should be well formed so that software implementing channels doesn't make errors parsing the stream.</another-tag> </dialect> <dialect ch='0'>This is a second message in the same channel.</dialect> <dialect ch='0'>Messages flow in both directions with the same ch.</dialect>
Any peer can close an open channel at any time.
To close a channel simply send a NULL message. This is different from an empty message. An empty message looks like this and DOES NOT close a channel:
Empty messages contain no content, but they are not NULL messages. A NULL message uses a closed tag and looks like this:
Note that generally, NULL (closed) elements are used as directives (verbs) or acknowledgements. In the case of a NULL channel element the directive is to close the channel and since the channel is considered closed after that no acknowledgement is expected.
Multiple channels in multiple dialects can be open simultaneously.
In all cases, but especially when multiple channels are open, messages are sent as complete units. This allows messages from multiple channels to be transmitted safely over a single connection.
Any line outside of a message is called chatter. Chatter is typically logged or ignored but may also be useful for diagnostic purposes, human friendliness, or for low level functions like connection setup etc.
A simple fictional example that makes sense…
A master application ===> connects to a child service. ---> (connection and startup of the slave process) <--- Child service 1.3.0 <--- Started ok, 20160210090103. Hi! <--- Debug messages turned on, so log them! ---> <service ch='0'><tell-me-something-good/></service> <--- <service ch='0'><something-good>Ice cream</something-good></service> ---> <service ch='0'><thanks/></service> ---> <service ch='0'/> ---> <service ch='1'/><tell-me-something-else/></service> <--- <service ch='1'/><i>I shot the sheriff</i></service> <--- <service ch='1'/><i>but I did not shoot the deputy.</i></service> <--- I'm working on my ch='1' right now. <--- Just thought you should know so you can log my status. <--- Still waiting for the master to be happy with that last one. ---> <service ch='1'><thanks/></service> ---> <service ch='1'/> <--- Whew! ch='1' was a lot of work. Glad it's over now. ... ---> <service ch='382949'><shutdown/></service> <--- I've been told to shut down! <--- Logs written! <--- Memory released! <--- <service ch='382949'><shutdown-ok/></service> <--- <service ch='382949'/> Not gonna talk to you no more ;-) Bye!