I’ve refactored Griffin.Network
quite a lot (I did say that it was still an early release). The new version is called v0.5 (the library is taking shape, but there are still some things to take care of).
Highlights
- The pipeline is no longer mandatory
- I’m using the Async methods instead of Begin/EndXxxx
- The buffer management is more efficient
- It’s easier to work with the buffers
- Introducing messaging support
Dead Easy to Get Started
If you just want to use the library, here is how you can configure it to send objects over the network using JSON and a small binary header:
public class Program
{
private static void Main(string[] args)
{
var serviceFactory = new MyServiceFactory();
var messageFactory = new BasicMessageFactory();
var configuration = new MessagingServerConfiguration(messageFactory);
var server = new MessagingServer(serviceFactory, configuration);
server.Start(new IPEndPoint(IPAddress.Any, 7652));
var client = new MessagingClient(new BasicMessageFactory());
client.Connect(new IPEndPoint(IPAddress.Loopback, 7652));
client.Received += (sender, eventArgs) => Console.WriteLine
("We received: " + eventArgs.Message);
client.Send(new OpenDoor {Id = Guid.NewGuid().ToString()});
Console.ReadLine();
}
}
The complete sample.
Let’s see how that works…
Messaging
Typically, you want to create an application that sends and receives messages over the network. It usually is a flow like this:
Receiving
- Receive
byte[]
buffers - Receive enough to be able to build a message (by using a length header or a delimiter)
- Build the message
- Process the message
Sending
- Do your magic
- Send a message
- Serialize it
- Send it asynchronously (might require several
SendAsync
)
Instead of having to do all those steps, there is now a Messaging server available. What you have to do now is simply to specify a message builder and a message serializer and you’ll automagically get to work with messages directly.
Sample Implementation
The first thing we have to do is to define our factory that creates serializers and message builders. Let’s decide that we should only use a binary header (version + length) and JSON to transport everything.
When we are sending things over the network with Griffin.Networking
, we’ll refer to messages instead of objects. For that’s what the library does. It serializes the objects, put them in a message (typically with a binary header) and sends everything to the remote end point.
Serializing Messages
To convert objects to byte arrays, we’ll use serializers. They look just like typical .NET serializers. Here is the one for the basic protocol:
public class BasicMessageSerializer : IMessageSerializer
{
private static readonly byte[] VersionBuffer = new byte[] {1};
public void Serialize(object message, IBufferWriter writer)
{
var str = JsonConvert.SerializeObject(message);
var bodyBytes = Encoding.UTF8.GetBytes(str);
writer.Write(VersionBuffer, 0, VersionBuffer.Length);
var buffer = BitConverter.GetBytes(bodyBytes.Length);
writer.Write(buffer, 0, buffer.Length);
writer.Write(bodyBytes, 0, bodyBytes.Length);
}
}
We first serialize the object as JSON and then start by writing the binary header. Finally, we also write the string
.
Building Incoming Messages
When receiving bytes from the socket, we’ll have to do more, since TCP sends everything in chunks. There is no guarantee that everything is received directly. Hence, we need build objects and not just try to deserialize them directly.
public class BasicMessageBuilder : IMessageBuilder
{
private readonly byte[] _header = new byte[Packet.HeaderLength];
private readonly Queue<Packet> _messages = new Queue<Packet>();
private int _bytesLeft = Packet.HeaderLength;
private Packet _packet;
private Func<IBufferReader, bool> _parserMethod;
public BasicMessageBuilder()
{
_parserMethod = ReadHeaderBytes;
}
public bool Append(IBufferReader reader)
{
while (_parserMethod(reader))
{
}
return _messages.Count > 0;
}
public bool TryDequeue(out object message)
{
if (_messages.Count == 0)
{
message = null;
return false;
}
var packet = _messages.Dequeue();
using (var reader = new StreamReader(packet.Message))
{
var str = reader.ReadToEnd();
message = JsonConvert.DeserializeObject(str);
}
return true;
}
protected virtual bool ReadHeaderBytes(IBufferReader stream)
{
var bytesLeftInStream = stream.Count - stream.Position;
var bytesToCopy = bytesLeftInStream < _bytesLeft
? bytesLeftInStream
: _bytesLeft;
stream.Read(_header, 0, bytesToCopy);
_bytesLeft -= bytesToCopy;
if (_bytesLeft > 0)
return false;
_packet = CreatePacket(_header);
_bytesLeft = _packet.ContentLength;
_parserMethod = ReadBodyBytes;
return true;
}
protected virtual bool ReadBodyBytes(IBufferReader reader)
{
var bytesLeftInStream = reader.Count - reader.Position;
var bytesToCopy = bytesLeftInStream < _bytesLeft
? bytesLeftInStream
: _bytesLeft;
reader.CopyTo(_packet.Message, bytesToCopy);
_bytesLeft -= bytesToCopy;
if (_bytesLeft > 0)
return false;
_packet.Message.Position = 0;
_messages.Enqueue(_packet);
_packet = null;
_bytesLeft = Packet.HeaderLength;
_parserMethod = ReadHeaderBytes;
return true;
}
protected virtual Packet CreatePacket(byte[] header)
{
var message = new Packet
{
Version = _header[0],
ContentLength = BitConverter.ToInt32(header, 1)
};
if (message.Version <= 0)
throw new InvalidDataException(string.Format(
"Received '{0}' as version.
Must be larger or equal to 1.", message.Version));
if (message.ContentLength <= 0)
throw new InvalidDataException(string.Format(
"Got invalid content length: '{0}',
expected 1 or larger.", message.ContentLength));
message.Message = new MemoryStream(message.ContentLength);
return message;
}
}
The Message Factory
To be able to customize the message handling, we’ll provide the serializer and the message builder through an abstract factory. It has two methods, one for creating serializers and one for creating message builders.
public class BasicMessageFormatterFactory : IMessageFormatterFactory
{
public IMessageSerializer CreateSerializer()
{
return new BasicMessageSerializer();
}
public IMessageBuilder CreateBuilder()
{
return new BasicMessageBuilder();
}
}
Working with the Messages
Do note that messaging is just one of the options that is included in Griffin.Networking
. You can for instance handle the byte arrays directly if you like to. The framework will still take care of all buffer handling, network IO and connection management for you.
Before creating the server, we have to define the class that will handle all incoming messages. Compare it to a WCF service:
public class MyService : MessagingService
{
public override void HandleReceive(object message)
{
var msg = (OpenDoor)message;
Console.WriteLine("Should open door: {0}.", msg.Id);
Context.Write(new DoorOpened(msg.Id));
}
}
All messages are getting into the same method since we haven’t specified what the server should do. Nothing says that it has to be an RPC server like WCF.
We can of course build something on top of the service to get an approach similar to WCF service classes.
Starting the Server
We’ve put all pieces together. Let’s create a server:
class Program
{
static void Main(string[] args)
{
var serviceFactory = new MyServiceFactory();
var messageFactory = new BasicMessageFactory();
var configuration = new MessagingServerConfiguration(messageFactory);
var server = new MessagingServer(serviceFactory, configuration);
server.Start(new IPEndPoint(IPAddress.Any, 7652));
Console.ReadLine();
}
}
Yay.
Example Client
We also need something which can communicate with the server.
var messageFactory = new BasicMessageFactory();
var client = new MessagingClient(messageFactory);
client.Connect(new IPEndPoint(IPAddress.Loopback, 7652));
client.Received += (sender, eventArgs) => Console.WriteLine("We received: " + eventArgs.Message);
client.Send(new OpenDoor{Id = Guid.NewGuid().ToString()});
Summary
That was a short introduction to the new messaging alternative of Griffin.Networking
. The pipeline that I described in the last Griffin.Networking
blog post is still included, but it’s not the only option any more.
As a bonus, here is a basic HTTP server:
internal class Program
{
public static void RunDemo()
{
var server = new MessagingServer(new MyHttpServiceFactory(),
new MessagingServerConfiguration(new HttpMessageFactory()));
server.Start(new IPEndPoint(IPAddress.Loopback, 8888));
}
}
public class MyHttpServiceFactory : IServiceFactory
{
public IServerService CreateClient(EndPoint remoteEndPoint)
{
return new MyHttpService();
}
}
public class MyHttpService : HttpService
{
private static readonly BufferSliceStack Stack = new BufferSliceStack(50, 32000);
public MyHttpService()
: base(Stack)
{
}
public override void Dispose()
{
}
public override void HandleReceive(object message)
{
var msg = (IRequest) message;
var response = msg.CreateResponse(HttpStatusCode.OK, "Welcome");
response.Body = new MemoryStream();
response.ContentType = "text/plain";
var buffer = Encoding.UTF8.GetBytes("Hello world");
response.Body.Write(buffer, 0, buffer.Length);
response.Body.Position = 0;
Send(response);
}
}
I’m currently writing a more complete server which will replace my old C# WebServer.
Documentation is also available, code at github.
Nuget:
install-package griffin.networking
install-package griffin.networking.protocol.basic
The post New version of Griffin.Networking – A networking library for .NET appeared first on jgauffin's coding den.