Griffin.Networking was my attempt to create a Netty inspired library. The library is working pretty well but the internal architecture became a bit complex which I am not really satisfied with. Griffin.Framework can now be considered to be a stable replacement of Griffin.Networking.
What’s Wrong with Griffin.Networking
I learned a lot when building Griffin.Networking. As it was my first attempt to create a somewhat performant library, it didn’t get very fast and the architecture wasn’t as elegant as it could have been. If I remember correctly, I managed to get 2000 http requests per second with it. I’ve just tried the same with my new library and I got 6000 requests per second. That’s 0,167 ms per request. (Do note that I used a trivial request containing a simple document, it’s just serving as a pointer.)
Below are the reasons as to why I felt that I had to redo the library.
Number of Allocations
One of the first things you have to reduce when trying to write a performant networking library is to reduce the number of allocations as the IO methods are going to be called a lot.
In Griffin.Networking, I tried to do that by doing micro management. That resulted in me creating different types of object pools and factories. That is great if you look at the smaller picture. But if you are using pipelines, a large number of allocations would be done each time a new pipeline is created. Even if you just created a socket channel, there were a lot of operations going on.
In the new library, I still have some buffers and a buffer pool, but the API is much more lightweight and I also take a different approach. I reuse the entire client channel which means that the amount of calls to pools, etc. are heavily reduced when a new client connects.
That also means that it’s much easier to build servers as you could typically just create a buffer (new byte[65535]
) in your class and still be sure that it’s going to be reused. When I later will reintroduce pipes (pipes and filters pattern), that simplicity is going to aid a lot.
Flexibility vs Complexity
In Griffin.Networking, I allowed service location / constructor injection in the pipeline to allow flexibility. In the new library, I’ve removed that. In my opinion, there should be minimal amount of work towards external sources in the networking layer. Instead, that work should be done in the layer above. My reasoning is that there will probably be a lot of operations in the network layer before a new application message is generated that can be act upon in the above layer. Thus, in the new library, I have no support for IoC container as it’s up to the application layer to handle that.
There are two extension points in the new library.
The first point is the ITcpChannelFactory
which are used to create the channels that are used for communication. In that way, you can get a fine grained control over what goes on in the IO operations.
The second point is that the library uses encoders and decoders on the incoming data. There are some built in encoders. For instance, for the STOMP messaging protocol and of course for HTTP. Each time your create a client or a server, you specify which encoder/decoder to use.
To further simplify the library, there may now only be protocol implementations in the library. If something even smells as a server, it should be moved to a separate library. I’m therefore pushing some parts of the HTTP implementation to the Griffin.WebServer project.
Channels
The new library is still focused around channels. A channel takes care of the IO operations and is represented by an interface:
public interface ITcpChannel
{
DisconnectHandler Disconnected { get; set; }
MessageHandler MessageReceived { get; set; }
MessageHandler MessageSent { get; set; }
DecoderFailureHandler DecoderFailure { get; set; }
EndPoint RemoteEndpoint { get; }
string ChannelId { get; }
IChannelData Data { get; }
void Assign(Socket socket);
void Cleanup();
void Send(object message);
void Close();
}
There are two types of built in channels: TcpChannel
and SecureTcpChannel
(SSL).
The great thing is that the architecture is build so that those two classes can be used both at server side and client side, thus making it easier for other developers to build implementations on top of Griffin.Framework.
Building a Server
What is required to build a server than? Not much.
Here is a fully working implementation of a server:
public class Server
{
private readonly ChannelTcpListener _server;
public Server()
{
_server = new ChannelTcpListener();
_server.MessageReceived += OnMessage;
_server.ClientConnected += OnClientConnected;
_server.ClientDisconnected += OnClientDisconnected;
}
public int LocalPort
{
get { return _server.LocalPort; }
}
public void Start()
{
_server.Start(IPAddress.Any, 0);
}
private void OnClientConnected(object sender, ClientConnectedEventArgs e)
{
Console.WriteLine("Got connection from client with ip " + e.channel.RemoteEndPoint);
}
private void OnClientDisconnected(object sender, ClientDisconnectedEventArgs e)
{
Console.WriteLine("Disconnected: " + e.Channel.RemoteEndpoint);
}
private void OnMessage(ITcpChannel channel, object message)
{
Console.WriteLine("Server received: " + message);
channel.Send("Well, hello you too");
}
}
The default listener uses a build in message format called MicroMsg
. It basically adds a content length and type information into a header. The great thing with MicroMsg
is that it supports streams out of the box. You can for instance send a FileStream
containing a 10 gigabyte large file without increasing the memory footprint of your application.
Custom Protocol
The protocol implementation will be demonstrated later, but it’s easy to switch protocol:
public class Server
{
private readonly ChannelTcpListener _server;
public Server()
{
var config = new ChannelTcpListenerConfiguration(
() => new MyProtocolDecoder(),
() => new MyProtocolEncoder()
);
_server = new ChannelTcpListener(config);
_server.MessageReceived += OnMessage;
_server.ClientConnected += OnClientConnected;
_server.ClientDisconnected += OnClientDisconnected;
}
}
See? The configuration contains two factories. One to construct an encoder and one to construct a decoder. Do note that the factories are only called if there are no channels available for reuse.
Custom Channels
You can also customize which channels the library should use by changing the ChannelFactory
property:
server.ChannelFactory = new SecureTcpChannelFactory(new ServerSideSslStreamBuilder(myCertificate));
Fully Asynchronous
The library is fully asynchronous both in the server side and the client side. The server does however not support TPL yet. Instead, you have to use your own callbacks.
Example:
private void OnMessage(ITcpChannel channel, object message)
{
BeginDoSomething(message, OnEndDoSomething, channel);
}
public void OnEndDoSomething(IAsyncResult ar)
{
var channel = (ITcpChannel) ar.AsyncState;
channel.Send("Sometrhing");
}
The server doesn’t act before you invoke channel.Send()
and you can do that at any time (as long as the channel is connected). Do note however that the server will continue to read from the channel and invoke the OnMessage
delegate for every new message, even if you have not replied to the first.
Channel Storage
In more complex solution, you might want the ability to store information specific for a channel (i.e. client connection). I support that by the channel.Data
property which basically wraps a ConcurrentDictionary
.
Example:
private void OnMessage(ITcpChannel channel, object message)
{
channel.Data["CurrentMessage"] = message;
BeginDoSomething(message, OnEndDoSomething, channel);
}
public void OnEndDoSomething(IAsyncResult ar)
{
var channel = (ITcpChannel) ar.AsyncState;
var msg = channel.Data["CurrentMessage"];
channel.Send("Sometrhing");
}
Clients
There is now a built in client in the library. It’s fully asynchronous using TPL and async
/await
. It uses per default the MicroMsg
protocol. Here is a fully working client:
var client = new ChannelTcpClient<object>();
await client.ConnectAsync(IPAddress.Loopback, serverPort);
await client.SendAsync("Hello world");
var response = await client.ReceiveAsync();
await client.CloseAsync();
To talk with a HTTP server, you can construct a client like this:
var client = new ChannelTcpClient<IHttpResponse>(new HttpRequestEncoder(), new HttpResponseDecoder());
await client.ConnectAsync(SomeIp, 80);
await client.SendAsync(new HttpRequest("GET", "/", "HTTP/1.1"));
var response = await client.ReceiveAsync();
Console.WriteLine(response.StatusCode);
Note that’s it’s just a demonstration of how you can construct server/clients just as long as there are encoders/decoders. Basically building encoders/decoders are the only thing you need to do to support new protocols, no need to even touch the networking code.
Custom Protocol
Here is a custom protocol which only adds a length header and a string
body (JSON). Remember that TCP is stream based and we therefore have to make sure that everything is sent/received.
The encoders have to take into account that all bytes might not be sent, as TCP is stream based.
public class MyProtocolEncoder : IMessageEncoder
{
private readonly MemoryStream _memoryStream = new MemoryStream(65535);
private readonly JsonSerializer _serializer = new JsonSerializer();
private int _bytesLeft;
private int _offset;
public void Prepare(object message)
{
_memoryStream.SetLength(0);
_memoryStream.Position = 4;
var writer = new StreamWriter(_memoryStream);
_serializer.Serialize(writer, message, typeof (object));
writer.Flush();
BitConverter2.GetBytes((int) _memoryStream.Length - 4, _memoryStream.GetBuffer(), 0);
_bytesLeft = (int) _memoryStream.Length;
_offset = 0;
}
public void Send(ISocketBuffer buffer)
{
buffer.SetBuffer(_memoryStream.GetBuffer(), _offset, _bytesLeft);
}
public bool OnSendCompleted(int bytesTransferred)
{
_offset += bytesTransferred;
_bytesLeft -= bytesTransferred;
return _bytesLeft <= 0;
}
public void Clear()
{
_bytesLeft = 4;
_memoryStream.SetLength(0);
_memoryStream.Position = 0;
}
}
The decoder has to take into account that messages might be partial or that the incoming stream contains several messages.
public class MyProtocolDecoder : IMessageDecoder
{
private readonly byte[] _headerBuf = new byte[4];
private readonly JsonSerializer _serializer = new JsonSerializer();
private readonly MemoryStream _stream = new MemoryStream();
private int _bytesLeft;
private int _headerBytesLeft = 4;
private int _headerOffset = 0;
private bool _isHeaderRead;
public void ProcessReadBytes(ISocketBuffer buffer)
{
var offset = buffer.Offset;
var count = buffer.BytesTransferred;
if (!_isHeaderRead)
{
var headerBytesRead = Math.Min(_headerBytesLeft, count);
Buffer.BlockCopy(buffer.Buffer, offset, _headerBuf, _headerOffset, headerBytesRead);
_headerBytesLeft -= headerBytesRead;
if (_headerBytesLeft > 0)
return;
count -= headerBytesRead;
offset += headerBytesRead;
_bytesLeft = BitConverter.ToInt32(_headerBuf, 0);
_isHeaderRead = true;
}
var bodyBytesToRead = Math.Min(_bytesLeft, count);
_stream.Write(buffer.Buffer, offset, bodyBytesToRead);
_bytesLeft -= bodyBytesToRead;
if (_bytesLeft == 0)
{
_stream.Position = 0;
var item = _serializer.Deserialize(new StreamReader(_stream), typeof (object));
MessageReceived(item);
Clear();
}
}
public void Clear()
{
_bytesLeft = 0;
_headerBytesLeft = 4;
_headerOffset = 0;
_isHeaderRead = false;
_stream.SetLength(0);
_stream.Position = 0;
}
public Action<object> MessageReceived { get; set; }
}
HTTP
Here is a sample HTTP server:
class Program2
{
static void Main(string[] args)
{
var listener = new HttpListener();
listener.MessageReceived = OnMessage;
listener.BodyDecoder = new CompositeBodyDecoder();
listener.Start(IPAddress.Any, 8083);
Console.ReadLine();
}
private static void OnMessage(ITcpChannel channel, object message)
{
var request = (HttpRequestBase)message;
var response = request.CreateResponse();
if (request.Uri.AbsolutePath == "/favicon.ico")
{
response.StatusCode = 404;
channel.Send(response);
return;
}
var msg = Encoding.UTF8.GetBytes("Hello world");
response.Body = new MemoryStream(msg);
response.ContentType = "text/plain";
channel.Send(response);
if (request.HttpVersion == "HTTP/1.0")
channel.Close();
}
}
Using SSL is really easy, just change the channel factory:
var certificate = new X509Certificate2("GriffinNetworkingTemp.pfx", "somepassword");
var listener = new HttpListener();
listener.ChannelFactory = new SecureTcpChannelFactory(new ServerSideSslStreamBuilder(certificate));
Here is a benchmark that I did on a small HTML document:
Custom Protocol
Here is a sample implementation which a custom protocol (length header + JSON). It can send any type of object over the network (as long as JSON.NET can serialize it).
class Program
{
static void Main(string[] args)
{
var config = new ChannelTcpListenerConfiguration(
() => new MyProtocolDecoder(),
() => new MyProtocolEncoder()
);
var server = new ChannelTcpListener(config);
server.MessageReceived += OnMessage;
server.Start(IPAddress.Any, 0);
ExecuteClient(server).Wait();
Console.WriteLine("Demo completed");
Console.ReadLine();
}
private static async Task ExecuteClient(ChannelTcpListener server)
{
var client = new MyProtocolClient();
await client.ConnectAsync(IPAddress.Loopback, server.LocalPort);
await client.SendAsync(new Ping{Name = "TheClient"});
var response = await client.ReceiveAsync();
Console.WriteLine("Client received: " + response);
}
private static void OnMessage(ITcpChannel channel, object message)
{
var ping = (Ping) message;
Console.WriteLine("Server received: " + message);
channel.Send(new Pong
{
From = "Server",
To = ping.Name
});
}
}
Conclusion
The library is available in nuget “griffin.framework” and the source code is available at github.