🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Network Library that supporst RPCs

Started by
19 comments, last by teremy 8 years, 2 months ago

MethodInfo aren't very fast to invoke, either. If you had a hardcoded switch statement, it would be much faster. Although since the RPC already has the delay of being transferred over the network, a MethodInfo.Invoke probably won't factor into performance noticably.

Well if I want to communicate from client to server or vice versa, then there always has to be transferred something over the network. There is always a delay, whatever syntax I use. There is not really an alternative to using MethodInfo.Invoke, right? If I use hardcoded switch statements than I guess we are not talking about RPCs anymore. I like the RPC syntax, because there is a different method for every different message. So it's easily maintainable. There is no way to use delegates instead of invoking a MethodInfo, right? Or I could use reflection to generate a switch statement out of all the rpc methods at program start ( definitely won't do this :D ). I once used Unity's UNET for a fast paced bomberman clone, that of course demanded a very fast communication process. Had no problems, even though using their RPC stuff ( so I guess invoking a method might not be the most performant way compared to the hardcoded switch statement, but it's still more than fast enough for fast online games that are very time-sensitive ).

Advertisement
You can use delegates instead of MethodInfo, I think:

https://msdn.microsoft.com/en-us/library/53cz7sc6(v=vs.110).aspx

Great, that was my original idea.

Guess I start doing some programming now.

Thanks again guys!

there always has to be transferred something over the network


True, but your job is to minimize what that is.
For example, giving each method a small integer instead of a string name, would reduce the size needed in the network packet.
Similarly, building a wrapper that introspects the server code, and generates a class that exposes exactly the methods that the server exports, and then marshals the arguments using known-types (only the data) will save a lot of space over marshaling arguments with type information like default .NET Serialization does.
You should aim for the overhead of a method invocation to be a few bytes (say, five at most,) and for each argument in an invocation to only need the size of the data (4 bytes for an int, num-chars bytes for a string, etc.)
enum Bool { True, False, FileNotFound };

Maybe one last question.

As I said, the rpc message, that was sent for my master server stuff was something like "methodname:parameter1:parameter2:parameter3...", it also contained the string "<EOM>" at the end, because since tcp ( that was the protocol I used for my master server stuff ) is streambased, I had the option to either tell the message size at the beginning of the message, or to use a delimiter ( "<EOM>" in my case ) to indicate the end of the message.

A problem with this is of course, that the message itself must not contain this delimiter, or else the message gets cut at this point and we have two messages of garbage.

There's also the delimiter I used between the methodname and the different parameters ( ":" ).

I could of course have a static packet size, so I know which bytes belong to which parameter, but it would be more efficient to have a variable packet size, so only the information is sent, that is needed ( for example a packet of a chat message, that simply contains "hi" should be much less in size than the packet of a chat message containing "blablablablablablablablablablablablablablabla....", in reality I will of course have a maximum chat message length or send a very long chat message with multiple packets, but that's another topic ).

I took a look into protobuf and my current serialization method was to simply convert everything into a string and use the getBytes() method ( which is still much less data than using JSON ). But there are tons of great and efficient serialization librarys out there, like protobuf or msgpack. I'm thinking of using one of those, but I still have to implement something, so the receiver knows which bytes belong to which parameter.

As I did before, I could put a ":" ( in byte format of course ) after a parameter. Is this the right way todo this? It would work, however to make this robust ( same with the message delimiter ) I would need to escape any occurences of this in the bytes of the parameters ( and deescaping might be a bit tricky ).

Any better way todo it? This way a parameter can have a variable size (depending on the actual value), which is the case if I serialize an integer like 1 or 20000 with the above mentioned serialization librarys.

A problem with this is of course, that the message itself must not contain this delimiter


Which is why most game protocols use the size-first approach. Also, for efficiency, they typically marshal data as binary, rather than as text.

For formats that have to be textual by convention or definition (email, JSON, etc) the two options are to either encode your payload as text using something like base64 encoding, or introducing a quoting mechanism.
For example, URLs use a quoting mechanism where "%xx" will replace a URL-reserved character (slash, ampersand, equals, percent, space, etc) with its hexadecimal equivalent.
You could do this as well -- whenever you see a '<' or a ':' or a '%' in the payload, insert the appropriate %xx code. Then, in the protocol decoder, when you see a %, read the next two characters and decode. When you see a '<' or a ':' you know that it's part of the protocol.

All that being said -- I think the way you're doing this is not optimal, and the netcode that you're working on will never be "great" except possibly for slow, turn-based games like chess or whatever.
If that's OK by you, then you should keep going. If you want "great" netcode for games with significant data volume or significant latency requirements, you need to take the advice in this thread about using binary marshaling and reducing the size of data that you need to send.
But, not everything needs to be "great," because "good enough and shipping" beats "great and not shipping" :-)
enum Bool { True, False, FileNotFound };
Definitely put the size first. It's so much easier than worrying about delimiters, EOM indicators and dealing with escaping other data somehow.

Really the only nuisance part of doing size first is you have to build up your whole message in a temporary buffer first, then measure its length immediately before sending. But that's not nearly as bad as dealing with escaping data (which inevitably leads to bugs).

If you use a binary format you don't have to worry about any kind of string escaping or delimiters. Your data is arranged in consistent blocks with the leading information letting you know exactly how much data comes next, either using lengths, or type IDs, or any other deterministic approach.

Of course my current code is not optimal, it's what I wrote for my master server stuff and my main goal was to just get it to work. The good part is, that I can make changes to the rpc engine at anytime, but don't have to make changes to the client or server itself.

The reason I created this thread was to gather some information of what's the best or at least a better approach than what I am currently doing on my master server. I want to have a viable solution for fast paced games.

Let's say I go for the approach, that puts the packet size first. Now the receiver can tell which parts belong to a packet and btw. even with the EOM indicator, I still have to work with a buffer, since tcp is streambased and although the transfer is reliable, maybe just parts of it have arrived at a given time.

Still I don't know how do I know which bytes belong to which parameters? The first byte(s) of the message will tell the size of the whole packet, but how does the receiver figure out which bytes belong to which parameter? If I send 2 integers for example, how does the receiver know which bytes belong to the first and which belong to the second one? Should I add a byte before every parameter that tells the size of the parameter? Since I want things to be most efficient I'm not working with a fixed parameter size.

So sending an rpc message would be something like: byte for packet size + byte for the method to call + byte for size of parameter 1 + byte(s) of parameter 1 + byte for size of parameter 2 + byte(s) of parameter 2+...

I don't really see a more efficient way? Is this the right approach with prepending each parameter ( that can have many bytes, depending on what the parameter contains, like a small number or a long string ) with a byte of its size?

EDIT: This approach would also be very easy to implement and I can just serialize stuff with msgpack ( or another great serialization method ), check the bytesize and add the bytesize + the resulting bytes of the serializer to the packet. The max. size of a parameter with this approach would be 256, if all 8 bits of the byte, that indicates the parameter's size, are set.

For most types, the sender and receivers both know how big they are based on agreed-upon information. You typically do NOT need to send the size of every field.

For example, you could write code that agrees that all 32-bit integers are 4 bytes. Or you could agree to write them using SQLite4 varint encoding, or VLQ encoding, or whatever.

Let's look at a bare-bones BinaryReader/BinaryWriter pair in C#:


public class Foo
{
    public int A;
    public int B;

    public void Read(BinaryReader reader)
    {
        A = reader.ReadInt32();
        B = reader.ReadInt32();
    }

    public void Write(BinaryWriter writer)
    {
        writer.Write(A);
        writer.Write(B);
    }
}
The Read and Write functions don't need to write bytes indicating that A is 4 bytes and B is 4 bytes, because it's implicit.


You only really need lengths for two main things:

1. Knowing how much data to wait for in your network receiver before you attempt to deserialize the whole message.
2. Skipping fields of unknown types if you have a protocol that allows for different client versions to talk to each other. You probably won't need this, though.


For RPC, you could do something like this (very barebones example):


public void HandleRPC(BinaryReader reader)
{
    var function = (RPCFunction)reader.ReadInt32(); // RPCFunction being an enum

    switch (function)
    {
        case RPCFunction.ConsoleWriteLine:
        {
            // This is a bit of a security risk if you were to do this in C++, but in C# the worst you'll get is an exception.
            var formatString = reader.ReadString(); // Internally it has the string length first
            var numArgs = reader.ReadInt32();
            object[] args = new object[numArgs];
            for (int i=0; i<numArgs; ++i)
                args[i] = ReadPolymorphicType(reader);
            
            Console.WriteLine(formatString, args);
        }

        // Other functions read their expected parameters, then call the appropriate function in the same way.
    }
}

public object ReadPolymorphicType(BinaryReader reader)
{
    int typeId = reader.ReadInt32();
    switch (typeId)
    {
        case 0: return null;
        case 1: return reader.ReadByte();
        case 2: return reader.ReadInt32();
        case 3: return reader.ReadSingle();
        case 4: return reader.ReadDouble();
        case 5: return reader.ReadString();
        case 6: { var foo = new Foo(); foo.Read(reader); return foo; }
        // etc.
    }
}
This is a manually-written, tedious way to go about it, but it serves as an example for how you can serialize/deserialize data. You would ideally want to use whatever is most convenient for your project.

I would personally use varints for all integers, have string backreference support, etc.

you have to build up your whole message in a temporary buffer first, then measure its length immediately before sending


You want to do that anyway, because you don't want to call send() multiple times for as single packet, because system calls have a cost.
Just leave a few bytes empty at the beginning of your buffer, and after you know the size, pre-fill that at the head of the buffer, then call send() on the entire thing.

I still have to work with a buffer


Yes; any TCP receiver has to call recv() into the end of whatever buffer it keeps around, and then try to decode as many whole packets as possible out of that buffer, and then move whatever is left to the beginning of the buffer.
(Cyclic buffers are sometimes convenient for this.)

how do I know which bytes belong to which parameters?


Decode each argument in order. You presumably know the type of each argument.
The simplest method: If the type is an int, it's 4 bytes. If the type is a double, it's 8 bytes. If the type's a string, then make the rule that strings are less than 256 bytes long, and send a length byte, followed by that many bytes.
When you have decoded the correct number of arguments, you know that the next part of your message follows.
If you support variable number of arguments, first encode the number of arguments to decode, using another byte.

Less simple methods will decode integers as some kind of var-int, floats using some kind of quantization (based on what the particular value is,) strings using a dictionary that gets built up over the connection, etc.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement