Helios 2.0 APIs

The general workflow for developers intending to consume Helios 2.0 is as follows:

  1. Define a Helios IChannel object, which represents a duplex channel for interacting with an arbitrary socket implementation (ITransport.) IChannels can be configured and bootstrapped to support different transports with different types of configuration options, including TLS support.
  2. An IChannel will contain an IChannelPipeline, which defines a duplex pipeline of various IChannelHandler implementations who will be invoked in sequential order during events emanating from or TO the socket, depending on how the IChannel object is called.
  3. All operations called on the socket will themselves be asynchronous and will return a ChannelTask, which is really just a wrapper for Task<ChannelContext>.

Here's a diagram that illustrates what this duplex pipeline would look like under a typical scenario:

Helios `IChannel` with `IChannelHandler` pipeline wrapped around a socket transport

What sorts of things will IChannelHandler do?

An IChannelHandler implementation can be anything that occurs during the normal process of working with a socket:

  • Decoding and encoding messages (framing;)
  • Encrypting and decrypting messages;
  • Serializing and deserializing messages;
  • Authentication and authorization; and
  • Performing any arbitrary application-level work with deserialized messages.

An IChannelHandler can be anything involved in the process of working with each remote host connected to the underlying ITransport - and Helios will provide a number of built-in IChannelHandler implementations for performing routine functions such as message framing.

How many IChannel instances are created per ITransport?

In server-side scenarios, ServerChannels have the ability to create child IChannel instances for each remote host - this allows server-side developers to write their IChannelHandler implementations for working with a single host, thus an AuthenticationChannelHandler only needs to worry about authenticating a single user, versus authenticating potentially thousands.

This is, fundamentally, a much simpler programming model.

On the client-side, a developer can open a single SocketChannel and it will have precisely one IChannel instance.

IChannels run on a single thread

Each IChannel instance processes one batch of messages at a time, which gives us the following benefits as developers:

  1. We can maintain state that is unique to the remote host on the other end of the socket inside each IChannelAdapter implementation and have it automatically be thread-safe;
  2. We can safely add or remove IChannelHandlers from the IChannelPipeline on a per-host basis; for instance, if a user authenticates we can remove the AuthenticationChannelHandler from the pipeline and replace it with a AuthenticatedChannelHandler going forward.

IChannel won't begin reading new messages from the socket until its finished processing its last read

Consider the following scenario:

  1. 200 bytes arrive on a TCP socket between reads, and each contains 10 framed messages (4 byte header + 16 byte message.)
  2. The IChannel fires its ChannelRead() operation, which passes the 200 bytes to a LengthFrameDecoder (implements IChannelHandler) step in the IChannelPipeline.
  3. The LengthFrameDecoder pulls each of the individual frames out of the 200 byte buffer and passes those messages one call at a time to the next IChannelHandler in the pipeline, a JsonSerializer step.
  4. The JsonSerializer deserializes a message into a usable C# object, and calls the next step in the pipeline -a user-defined IChannelHandler implementation.
  5. The user-defined IChannelHandler does something with the C# object passed into it from earlier, and the operation is complete for that individual message.
  6. This process gets repeated 9 more times for each of the messages that were decoded by the LengthFrameDecoder.

Before we can read any additional bytes from the socket, we have to finish processing all of the individual messages decoded by the LengthFrameDecoder (or write our own IChannelHandlers that can use them in batch.)

Here's what that process might look like, animated:

Helios decoding pass

All IChannelHandler implementations can respond to multiple types of channel events

There are lots of different types of events that may occur in within the channel:

  1. Read
  2. Write
  3. Open
  4. Bind
  5. Close
  6. Disconnect
  7. User-defined events.

Each IChannelHandler must have an implementation for each of the event handlers for these types of events, although some IChannelHandler implementations may choose not to do much of anything during some of these events. What's a LengthFrameEncoder going to do during a close event?

The IChannelHandler interface looks like this:

public interface IChannelHandler{
    void ChannelRead(ChannelHandlerContext context, object msg);
    void Write(ChannelHandlerContext context, object msg, ChannelPromise promise);
    void Connect(ChannelHandlerContext context, IpAddress address, ChannelPromise promise);
    void Disconnect(ChannelHandlerContext context, ChannelPromise promise);
}

N.B. This is not the final design of the IChannelHandler interface, but an approximation of it for the sake of documenting the spec.

An implementation of this interface that didn't handle any outbound writes, such as a decoder / deserializer, might have the following implementation:

public class DownStreamChannelHandler : IChannelHandler{
    public void Write(ChannelHandlerContext context, object msg, ChannelPromise promise){
        context.Write(msg, promise); //forward msg onto the next handler in the pipeline
    }

    // other IChannelHandler implementations
}

All operations against an IChannel are asynchronous