Cdh.SimpleRpc

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:

  1. 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.
  2. It must be relatively efficient on the wire. Protocol chatter should be minimal in comparison to the data being exchanged.
  3. 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.