Advertisement

Network Library that supporst RPCs

Started by April 27, 2016 12:17 AM
19 comments, last by teremy 8 years, 6 months ago

Hello.

I am making games in unity, however I dislike unity's network library ( UNET ).

What I need is a network library that is build on top of UDP and lets you send packets reliably if required and also support RPCs.

For my master server I simply used tcp sockets ( it's writen in c# ) and wrote my own RPC logic using reflection ( is there even another way to make RPCs? I basically send the method name and its parameters and on the receiving side I invoke that method with the parameters. This works good, still need some work on security and performance though ).

But for the game server/client I currently use UNET and UNET doesn't exactly allow RPCs. You can't call a method on a specific client ( I think they will include this option in one of the future version but there are also other things I don't like about UNET, mainly the architecture you are compelled to use. Another way was to use network messages and basically create a class for each different message type but that is way to much work.

I found RPCs are the easiest way to communicate. I read something about lidgren networking, but this doesn't support RPCs right? Any idea what I could use? Or should I program my own RPC logic on top of an existing networking library?

Even though I created my own RPC logic before, is there some sort of tutorial or a good description of an implementation? I know the implementations can be different, so if I have to make my own, I want to at least make a good one.

Thanks for your time!

The only "magic" part of UNET is that they replace the body of the sender's RPC method with a replacement that performs parameter serialization and sends the call, and generate the receiver-side deserialization code to call the function for you.

To do RPC manually, you just need to:

- Serialize parameters and function context (if applicable, like the gameObject's unique ID or whatever)
- Send the serialized information over the network like any other network data
- On the receiving end, deserialize the parameters and then call the appropriate function in the appropriate context (if applicable) using lookup tables (or reflection - it's debatable whether this is OK or not in the context of RPC - you maintain less code with reflection anyway).

If you want to handle returning a value back to the caller, you need an additional ID for the call itself, and a lookup table of code to call when you get a response. Since Unity doesn't have C# 5.0 await support yet, I would use a lambda-style continuation.
Advertisement

Thank you for your answer.

In my RPC implementation for my tcp sockets, the sender makes a call like sendRPC("Methodname", methodParameter1,methodParameter2, ... );

Internally this gets send as a string in the form of Methodname:methodParamter1:methodParameter2...

The receiving side will dismantle that string and cast the parameters to the respective type of the method's paramater.

It works only with basic types, since they can easily put and gotten from a string.

I think the reflection part can be made efficient by using a dictionary string -> method.

So reflection is used only at program startup to populate the dictionary.

The difficult part about RPCs is the serialization. For example if you want to send a List as a parameter. There are great and efficient ways to serialize an object, but the internal message, that is send when making an rpc, contains multiple parameters and the method name, the receiving side has to know which is which, and that I find a bit difficult. Took me some time to figure out how tcp works and that you don't even send messages, but its streambased, so I had to mark my messages, so the receiving side knows when a message ends etc... In the end it worked, but very basic.

For a more compact serialization, you should consider using a series of bytes instead of strings. For more fancy ways to make your network messages smaller, look at the encoding tricks that protobuf and SQLite4 use:

https://developers.google.com/protocol-buffers/docs/encoding
https://sqlite.org/src4/doc/trunk/www/varint.wiki

To handle more complex types, you write functions to read/write those complex types. For example with a list, you write the number of items in the list as a varint (-1 for null if you care about distinguishing between null and empty lists), followed by the serialized form of each element in the list. For a Dictionary<K,V> you send the number of pairs as a varint, then alternate between key,value,key,value.

For polymorphism you make a table of type IDs and send the type ID as a varint immediately before any polymorphic object.

For arbitrary graphs of objects, you traverse the graph, adding each object to a list the first time you see it, until you've seen all reachable objects. Then you write: # of objects, the type IDs for each object in the list, followed by the fields for each object in the list. Any time you encounter a field which is a reference type, you find that object in the list and serialize its index (you can use a Dictionary<object,int> to speed this up). Deserialization creates the object list, news each object by its type id, then fills in their fields and cross-references, then returns the first entry in the list as the "root" of the graph.


If you pass a function name over the network and use reflection to invoke it, it's a security risk because someone could send a network message like [mscorlib.dll:System.IO.Directory.Delete, "C:/Users", true] over the network, and you would *never* want to let your receiving side execute that.

To handle the issue of looking up the functions from method names, I suggest making part of your program startup register all of the possible functions you might want to use. Then, when you want to RPC that function, you look up its index in that table and send just the index over the network. Assuming both ends built the function table the same way, the receiver will know which function to use as well.

Thank you again, I will try to grasp everything you said.

About the security risk by invoking a function by name, I don't want to manually add every function to a list of methods, that are allowed to be invoked. I thought of maybe using attributes to declare a function as an RPC method, or to have a naming convention, so only methods with a name, that starts with "RPC" will be invoked. Maybe I will even use both methods. The example you posted of a malicious rpc message is really scary.

What I like about UNET is that when you use network messages ( create a class for each different type of network message ) it will automatically implement a serialize and deserialize method for it, but I guess I have to write those methods manually for a more complex type ( basically everything that is not a basic type like int, string, char, boolean etc. ).

The good thing if I write my own RPC implementation ( which includes the serialization/deserialization stuff ) I can easily switch the underlying network library.

Again thank you very much, this was very helpful.

I found RPCs are the easiest way to communicate.


RPC is convenient, but it does have known failure modes. If you synchronously block for an answer, for example, that's really bad from a performance point of view. If you assume that all function calls will succeed, that's another problem, as networks are unreliable. What does the RPC do when the client goes away?
Structuring game networking as a bidirectional stream of "fire and forget" updates is a LOT more robust than RPC.

I don't want to manually add every function to a list of methods


I don't want to manually have to lock my door when I leave the house, either, but I do it, because the cost is worth the benefit.

Note that, with default RPC function-and-arguments, a client can invoke any of the functions on your server with any argument at any time. For example, they can invoke "player X takes damage" at will, if there is such a function.
Sending a stream of messages and structuring your simulation to pay attention to (and validate) those messages, is, again, more robust and has less accidental-invocation surface area.
enum Bool { True, False, FileNotFound };
Advertisement

The RPCs I used on my master server are all not returning anything ( void ).

It's an authoritative server system, so the client only sends requests.

However my master server is also using tcp sockets and it's ok if things take some time.

A game server is of course something else. But I'd also use methods, that don't return anything ( void ), and therefore also don't block.

And it's ok if a function call fails ( packet is lost ... ). I will probably implement the possibility of reliably calling a method, which is simply done with the help of the underlying network protocol, that allows sending reliably sending udp packets ( and even lets you choose other options like in-order packets ).

What I meant by not wanting to manually add every function to a list of methods is that I want my RPC methods to be automatically put into the list, that contains the allowed methods to call. I could for example add an attribute to every RPC method or as I said, I could have a naming convention, so every method with methodname "RPC..." is allowed to be called over the network ( which means that its allowed to be invoked when the rpc message contains its methodname ).

This way they can't invoke a "player X takes damage" function, because it won't have an attribute to tag it as an RPC function or the methodname won't start with "RPC".

The syntax I used on my master server was something like SendRpc("Methodname", parameter1, parameter2...);

It wasn't Methodname(parameter1, parameter2...);, but on the receiving side, the respective method gets called with the parameters.

The server could also make RPCs in the form of SendRpc(clientId, "Methodname", parameter1, parameter2...);

When the server received an rpc message with "Methodname" and parameter1 etc., then the method "Methodname" gets invoked, but with the clientId as its first parameter, so the server can easily use the information from which client the message came. This is of course automatically done by my RPC engine ( or whatever you want to call it ), the clientId is not a parameter the sender has control of.

This was my basic implementation of RPC.

Reliabilty etc. I think is not even part of the RPC engine, it's part of the underlying protocol that is used and since I am going to use a udp based protocol with the ability for in order and reliable packet transfer, these properties will then of course be part of my RPC calls as well. It's pretty much just the syntax and structure of RPCs that I like, so I don't have to make a huge switch case for all the different network messages but rather have this done automatically by the rpc engine and invoke the corresponding method with its parameters.

I thought of maybe using attributes to declare a function as an RPC method, or to have a naming convention, so only methods with a name, that starts with "RPC" will be invoked.


Using attributes is a good way to do it. You just need to make sure that the way you scan for attributes always keeps the results in the same order. You could search for everything that has the attribute, and then alphabetically sort the search results so that they're always guaranteed to be in the same order.

I thought of maybe using attributes to declare a function as an RPC method, or to have a naming convention, so only methods with a name, that starts with "RPC" will be invoked.


Using attributes is a good way to do it. You just need to make sure that the way you scan for attributes always keeps the results in the same order. You could search for everything that has the attribute, and then alphabetically sort the search results so that they're always guaranteed to be in the same order.

Why do they have to be in the same order? Isn't it somehow possible to use a dictionary that contains the allowed rpc methods and gives me the method via the methodname as a string? Don't know if this is possible with delegates, but I've read reflection isn't really a performant way of doing this, I could use reflection only once at the program start to get all the allowed rpc functions and save them in a dictionary with methodname (string ) -> method ( delegate or something ). This way I can get the method in O(1) time and invoke it with the parameters...

Another thing I thought about was that the rpcmessage won't contain the methodname, but rather a number ( or a hash value or something ) that corresponds to a method ( it's also less data sent ), so when someone is looking into the packets, that are sent, he doesn't exactly know the methodname. The client however has to know those numbers then and since my code should be readable I have to create an enumeration or something so I still know which method is represented and I have to manually add to this enumeration if I add an rpc function. I guess I don't like to manually do stuff, when I think they can be automatically done, so the server has to provide some sort of interface for the rpc methods... but that's another topic....

Why do they have to be in the same order? Isn't it somehow possible to use a dictionary that contains the allowed rpc methods and gives me the method via the methodname as a string? Don't know if this is possible with delegates, but I've read reflection isn't really a performant way of doing this, I could use reflection only once at the program start to get all the allowed rpc functions and save them in a dictionary with methodname (string ) -> method ( delegate or something ). This way I can get the method in O(1) time and invoke it with the parameters...

Another thing I thought about was that the rpcmessage won't contain the methodname, but rather a number ( or a hash value or something ) that corresponds to a method ( it's also less data sent ), so when someone is looking into the packets, that are sent, he doesn't exactly know the methodname. The client however has to know those numbers then and since my code should be readable I have to create an enumeration or something so I still know which method is represented and I have to manually add to this enumeration if I add an rpc function. I guess I don't like to manually do stuff, when I think they can be automatically done, so the server has to provide some sort of interface for the rpc methods... but that's another topic....


If you use strings, it's O(1)... but you still have to transmit the whole string over the network (which will be larger than an integer), and the Dictionary<string,MethodInfo> will use GetHashCode on the string when looking up the dictionary entry. GetHashCode depends on the length of the string - it's pretty fast, but if you use integers you don't need it at all.

If you use integers, you can make a List<MethodInfo> instead and have an O(1) lookup with no hashing, and slightly less memory use than the Dictionary (the size of your function table will be insignificant compared to texture memory though, so don't worry about that).

You would still need a way on the sending side to find the function's ID before you know what integer to send over the network. In the first case you could reuse the same Dictionary and just send functions by name as well. In the second case you can look up the ID in any way you see fit (A Dictionary<string,int> for example).

In both cases, you would search for your functions once at startup and put them into the collection(s). After that you would use your function ID (either string or integer) to pick the method from the collection. If you use integers, you just need to make sure every client/server has the same IDs for the same functions. That's what I meant by the "in the same order" thing above.

Generally I only use Dictionary when the keys are sparse. If I have a Dictionary where the keys are all integers from 0 to N, I just use a List or possibly even an array instead.

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.

This topic is closed to new replies.

Advertisement