I’ve got this idea to code some game servers for a series of cooperative games my brother and I used to play as kids. I get similar ideas all the time… how about a game server for this card game or that board game? The problem I run into is pretty much always exactly the same: what communication protocol do I use?
I decided on a list of criteria that this protocol, whatever it is, must meet:
- It must be portable across programming languages and runtimes. If somebody else wants to write a better client using a different environment, that should be straightforward — perhaps not necessarily easy, but at least straightforward.
- It must be relatively efficient on the wire. Protocol chatter should be minimal in comparison to the data being exchanged.
- The object library should be simple and elegant to code against. When writing my game, the last thing I want to worry about is silly protocol details. Just get my message to the other computer please.
And here are the existing protocols I considered:
- .NET remoting. Since I code most in C# these days, it seemed like a logical choice. But it very blatantly breaks criterion 1 when using the binary formatter, and breaks both 1 and 2 when using the SOAP formatter.
- SOAP web service. Criterion 3 is satisfied, until you get to session persistence details. Criterion 1 is satisfied, and criterion 2… not so much.
- XML-RPC. Criterion 1 is met, and 2 is somewhat met. But criterion 3 is not — XML-RPC does not define any mechanism for dealing with persistent sessions. I would have to spend time writing a session manager with expiration and whatnot. No thanks.
And I’m sure I looked at others. The point is, for something as simple as message-passing between a game client and server, there doesn’t appear to be much out there that satisfies my requirements. And this is something I’ve come back to frequently.
Well, after several years of mulling the problem over in my subconscious, I knuckled down and coded. I have a usable library after two days of development. (And we’re talking maybe a few hours per day.) Written in C#, it allows any CIL-based language to write simple message-based client/server programs in very small amounts of code. For a quick example, let’s create a server that will convert strings to uppercase, with tracing back to the client.
First, we need to create an interface library so that the client and server know what each other’s methods are:
using System; using Cdh.SimpleRpc; namespace ServiceTest.Interfaces { public interface IServer { [RpcMethod] string ToUppercase(string str); } public interface IClient { [RpcMethod] void Trace(string message); } }
Now, here is the client:
using System; using System.IO; using Cdh.SimpleRpc; using ServiceTest.Interfaces; namespace ServiceTest.Client { public class MainClass { public static void Main() { Stream serverStream = ConnectToServer(); var service = new RpcService<IClient, IServer>(new Client(), serverStream); new Thread(delegate { while(service.Read()); }).Start(); IServer server = service.RemoteServerProxy; Console.WriteLine("ToUppercase result: " + server.ToUppercase("this is a test")); serverStream.Close(); } private Stream ConnectToServer() { // Here is your code to connect to the server endpoint. } } internal class Client : IClient { public void Trace(string message) { Console.WriteLine("Server trace: " + message); } } }
Note that the RpcService object generates a typed object that will transparently proxy calls to the remote service. The server program is almost as simple:
using System; using System.IO; using Cdh.SimpleRpc; using ServiceTest.Interfaces; namespace ServiceTest.Server { public class MainClass { public static void Main() { Stream clientStream = AcceptConnection(); Server server = new Server(); var service = new RpcService<IServer, IClient>(server, clientStream); server.client = service.RemoteServerProxy; while(service.Read()); clientStream.Close(); } private Stream AcceptConnection() { // Here is your code to accept a client connection. } } internal class Server : IServer { public IClient client; public string ToUppercase(string str) { client.Trace("Entering ToUppercase"); str = str.ToUpper(); client.Trace("Leaving ToUppercase"); return str; } } }
Ta-da. Some closing notes about this library:
- It should be completely thread-safe, and will allow you to place calls using the proxy objects from multiple threads. The calls will block until a response is returned from the remote service.
- Yes, you can throw exceptions in a service method, and yes, it will cause an exception to be thrown remotely from the proxy object.
- In the future it may be possible to flag service methods that return void as “no response” calls, which will cause the proxy call to return immediately. Of course, you will not be notified if an exception is thrown remotely.
- This API doesn’t do any complex serialization, and will only operate on the primitive types, excluding IntPtr. It will probably allow transmission of arrays at some point, and perhaps allow custom objects too.
Looking back on the list of criteria, this library, even in the early stages of development, easily meets all three. I’ll be hacking on it some more I’m sure, and may even publish the Git repository somewhere, when I’m confident that the code doesn’t totally suck.
Have you considered WCF?
I haven’t looked into it too much, but I very much doubt it would meet my first criterion.
Out of curiosity, why did you go with XML if #2 was a concern? Something 5 can very easily be replaced with (say) a 4-bit type, 4-bit length or value (15 indicating a following base-128 integer length or value), and then the data, if you really desire arbitrary type support. 5 would end up as one byte. 🙂
Granted, TCP’s enormous headers will probably dwarf that, but if you have a lot of updates that chatter may add up..
Hmm. The first ‘5’ was meant to be 5 .
Never mind. <args> is what I meant… perhaps it’ll go through this time.
I’m not sure what you’re saying exactly, unless you mean I should do some binary serialization. That’s not something I’ve ruled out entirely… I even wrote an AMF/AMF3 reader/writer recently that does very compact object serialization, but for right now I think XML is going to be the protocol of choice. It is a bit verbose, yes, but it’s very easy to debug and quite flexible.
Hey, my protocol isn’t SOAP. That’s gotta be worth something.
It seems to me that something like Apache Thrift is what you’re after.
Yeah, that looks pretty close. I’ll definitely take a look at it and see how it stacks up against my requirements. I’m especially interested in if it handles concurrent calls.
At a glance this seems pretty useful. My only immediate concerns would be the threading model… I could imagine some ugly surprises when a blocking call deadlocks (which presumably you are buffering the network traffic well enough that shouldn’t happen, but still)…
Even if it is thread-safe, I would probably want an assertion that void functions never block.
For those times when you want a bit more control (or conversely when you are making a very rare call and don’t need to setup an additional, explicit thread) I think it would also be useful to provide a “traditional” Async pattern as well: make it easy to follow the Begin*/End* pattern via IAsyncResult. (Possibly you could do some Proxy casting magic to do this. Of course things like that will be easier when C# gets the dynamic keyword.)
Just a few thoughts, hopefully they are useful. 🙂
> At a glance this seems pretty useful. My only immediate concerns would be the threading model… I could imagine some ugly surprises when a blocking call deadlocks (which presumably you are buffering the network traffic well enough that shouldn’t happen, but still)…
If a blocking call deadlocks, it will not affect any other calls. On the end responding to the call, the method is run on a separate thread. The worst-case scenario is that the server has an extra thread that doesn’t terminate. (And the other end, of course, never receives a response.)
> Even if it is thread-safe, I would probably want an assertion that void functions never block.
Void functions are just as capable of throwing exceptions as any other function. In cases where this is desired, they must block.
> For those times when you want a bit more control I think it would also be useful to provide a “traditional” Async pattern as well: make it easy to follow the Begin*/End* pattern via IAsyncResult.
Because the proxy object is implementing a specific interface, this would not make sense as things stand. One option might be that you can define such Begin/End methods on the interface and the proxy class will hook them up for you.
It’s also not too hard to use a Func<> or Action<> and BeginInvoke/EndInvoke.
And yes, comments are always helpful.