Pigeon
I'm still experimenting with building games, and one of my projects is a little client/server game. Rather than using WCF and dealing with the leaky abstractions, I decided to write something small and custom.
Pigeon is an alternative to WCF designed for high throughput.
- It uses raw TCP sockets
- It uses Google Protocol Buffers to keep messages small
- It is asynchronous
- The code is on BitBucket
On my local machine, WCF NetTcpBinding
maxes out at about 10,000 messages/second, while Pigeon achieves 40-50,000 messages/second.
Messages
Messages are encoded using Google Protocol Buffers. You just have to decorate your C# classes with the following attributes:
[Message(57)]
public class CreateCustomer
{
[MessagePart(1)] public string FirstName { get; set; }
[MessagePart(2)] public string LastName { get; set; }
[MessagePart(448)] public int Age { get; set; }
}
You don't have to use the same class library/DLL on the client and server. Instead,
the number 57 in the Message
attribute above is used to identify the type. So long as
the client and server have a type with the number 57, and attributes numbered 1, 2 and 448,
even if the classes have different names, it will just work.
Client example
First you configure the client - here I'm connecting to my loopback IP address on TCP port 90001.
var builder = new MessageClientBuilder("127.0.0.1", 90001);
builder.KnownTypes.Add(typeof(CreateCustomer));
var client = builder.Build();
We need the KnownTypes.Add
call to make the deserializer aware of the CreateCustomer
class, so that if
it is told to deserialize 57, it knows which class to create.
After we create the client, we can listen for messages from the server:
client.MessageReceieved += MessageReceieved;
client.Start();
...
private void MessageReceieved(object sender, object message)
{
Console.WriteLine("Got message: " + message);
}
The call to client.Start
creates a new background thread, which sits in a loop raising the
MessageReceieved
event each time a message is read from the TCP socket. Note that this means
your MessageReceived
handler will be called from a background thread.
Finally, the client can send messages to the server:
client.Send(new CreateCustomer { FirstName = "Paul" });
This will queue the message for sending by another background thread, leaving your application code to continue running uninterrupted.
Server example
Writing a server is a little more complicated, since you need to track which clients are connected, and send messages to specific clients.
The server is configured in a similar way to the client - it needs a TCP port number and known types:
var builder = new MessageServerBuilder(90001);
builder.KnownTypes.Add(typeof(CreateCustomer));
var server = builder.Build();
server.MessageReceived += MessageReceived;
server.Start();
...
private void MessageReceived(object sender, MessageReceivedEventArgs e)
{
var createCustomer = e.Message as CreateCustomer;
if (createCustomer != null)
{
// Create the customer
var newCustomerId = SaveNewCustomerToDatabase(createCustomer.FirstName);
// Reply back to the client (e.Sender), informing them that the customer
// was created
server.Send(new CreateSuccess(newCustomerId), e.Sender);
}
}
The server can also broadcast a message to all clients:
server.Broadcast(new HappyNewYear());
FAQ
How many threads are used?
A simple client application would use four threads:
- The main application thread
- The send thread, which sends outbound messages to the server
- The receive thread, which queues receieved messages from the server for dispatch
- The dispatch thread, which raises the
MessageReceived
event
A simple server application would also use five threads:
- The main application thread
- The listen thread, which accepts incoming socket requests
- The send thread, which sends outbound messages to any client
- The receieve thread, which queues receieved messages from the client for dispatch
- The dispatch thread, which raises the
MessageReceieved
event
Note that each of these threads sleep when there is no work to do
Will I run out of memory?
If your application is producing messages faster than they can be written to the sockets, or
if you are receiving messages from the socket faster than your MessageReceieved
event handler
can handle them, messages will be discarded. The memory usage should hit a limit, since there will
never be more than a fixed number of messages on the queue at once.
To illustrate, imagine an MMORPG. As the characters walk around the online world, they continually
call client.Send(new Moved(currentPosition))
messages to the server. Chances are, if the server is
struggling to cope with the number of messages, you'd be happy to discard the Moved
message that was
sent 20 seconds ago in favour of processing the Moved
message that was sent 1 second ago.