Advertisement

Handling large number of different packet types?

Started by April 28, 2006 10:08 AM
6 comments, last by cbirkhold 18 years, 9 months ago
I've been asked to develop a multiplayer game for someone. I do have some experience with networked apps, but I always run into 1 problem: How do I handle multiple types of packets in an elegant fashion, without turning the entire code into a complete mess? I'm looking for solutions that would work in OO languages, such as Java/C#, and that are elegant, yet easy to implement. I used to handle everything in a large switch before, but I've found that things turn into a giant mess when you need to process 10 or 20 messages. Also, how would you handle acknowledgement of packets? Do you reply to the server after each command? Or only for certain packet types? And what if you reply, do you use a special ACK message for it where the additional info is the sent package type, or a ACK_MSGTYPE? Do you cache up sent packets, in case you need to process ACKs or whatever? My main problem with developing networked apps is mainly that I mess up the protocol :). So hints/tips are welcome, articles on how to develop a clean protocol would be lovely! Toolmaker

There are two approaches - a 'close to the wire' approach where your packets themselves are the delivery unit, and a message approach where your packets are assembled to form longer messages.

Practicalities of packet format aside, what you're asking about is an efficient message structure. I'd implement a 1 byte code to identify the type of the message immediately (as in on reading the first byte, not 'do it now' ;- ) ). You can also identify based purely on size (a zero-data packet is often taken for an ack), or mix and match. As to which packets to ack, this is usually indicated by a flag in the packet header.

Regardless, once the transport part's over with, I check my message type code, and depending on that check length (if appropriate) and cast the entire message buffer to the appropriate message subclass. This is dangerous if you're not using byte aligned packing (#pragma pack 1) but otherwise it works nicely.
Winterdyne Solutions Ltd is recruiting - this thread for details!
Advertisement
What protocol? TCP or UDP? If it's TCP, then when the application sends an update packet, you can send the ID of the last packet you recieved. Since all TCP data is ordered and guaranteed, that implies that all messages prior to this one have been recieved.

As for the packet structure itself, I read 4 bytes when I get a message. 2 bytes are the ID, 2 bytes are the packet length. Then you can read length bytes, and enter a switch depending on the ID. It might end up quite big, but there's very little in the switch (Usually just calling another function).
Well, the usual way to get rid of large switch statements is to use virtual functions in C++ or something like a pointer table lookup in C. I'm sure for C# there are more elegant solutions than my C approach (pseudo code following)

typedef int (*msgfunc)(int msgtype, void* msgdata);msgfunc[NUM_MSG_TYPES] = { /* table of net message handlers */peer_connect,peer_disconnect,peer_chat,peer_shoot,};/* delegate a network message */recv(msgdata);char msgtype = msgdata[0];message_funcs[msgtype](msgtype, msgdata);


Does this help in any way?
Rather than use the "big switch statement" approach, there are several other possibilities, somewhat language dependent:

1. C++ - you can use an array of function pointers (or perhaps function objects?) to despatch the messages correctly - obviously index by message type ID. Just make sure it's bounds-checked properly.

2. C# - You can use a similar approach but use Delegates instead of function pointers

3. Java - Again, a similar approach, but use anonymous inner classes with an inline despatch function.

4. Any language which supports first-class functions (e.g. &#106avascript, Python (perhaps?)) - use an array of anonymous functions which simply call the appropriate despatch function

--

In C# or Java you could also use reflection to call a method based on the packet type ID with some kind of look up table with strings - the disadvantage of this method is that it's error prone as spelling mistakes etc, won't be detected at compile time.

Mark
Think of network message handlings as a serialization and deserialization problem, where compactness of storage is of utmost importance. There are tons of "OO" approaches to serialization.

A simple approach would be to register marshalling objects and message type handlers in a hash table, and send packets that contain <packetheader> followed by sequences of <messagecode> <messagedata>. On receipt, you decode the header (for authentication, timing, reliability and whatnot), and then get the message code. Look up the demarshaller for that code, call it (which consumes bytes from the packet), and pass the resulting opaque, demarshalled state to the handler event.

This approach would work well for C, C++, C# and Java, AFAICT. It's simplistic, but should scale to maybe 100 packet types without too much headache. Over that, you need custom solutions, like RPC descriptions.
enum Bool { True, False, FileNotFound };
Advertisement
Last time i did a networked message system, i had an array with the sizes of the variables [so i could pass pointers dependably] indexed by message type. Found a fixed number of parameters as an upper limit, and kept a table with the number of applicable pointers, and the element sizes of each, in a table sorta like this [had a maximum of 4 pointers]
Type   PCount    totalSize   v2   v2   v3   MOVE   2         16          12   0    0LOOK   1         12          0    0    0TALK   2         32773       4    0    0     [note that total size is 2^15 + 5, 4 for the type of speech, color,modifiers, ect, and at least one bit to hold a null]ect


When reading packets, i read in the type and the total size, checked the total size on the table to make sure it's a valid size, drew that size into a buffer, and references the different variables by the displacements given in v2, v3, v4, as indeces into the buffer size of the read in buffer. [so for a total size of 16, like with MOVE, the first variable pointer would point at buffer[0], the second variable pointer would point at buffer[12], and so on].

Variable sized parameters[like speech] were always kept in the last applicable pointer, with it's start position marked, and the size having a specific bit set [like the 32K bit, like talk, since it's silly for a single message to be of that huge of a size, and the actual size of the packet with the exception of the variable sized field was expressed as if that bit was off, as a minimum size] and was considered as a means of allowing irregularly sized packets to slip through [not be rejected based on bad size values]. This just marked it to use the incoming packets size value, and reject the table stored value for that packet type [like text], but still use the table size as a minimum [with the flag bit off]. Worked pretty well for me, but it was my first shot into networked programming with messages. Was simple and easy to manage, but probobly doesn't have the flexability that you'd want in a realy robust system.

This method did however, make for very easy packet construction, as the static sizes were all just copied into the respective spots on a pre-made buffer, and all the 'sizeof' junk went out the window, and objects did not have the pack their own data beyond the form inwhich they would use it naturally. This means that it wasn't as compact as it coulda been though....

It isn't perfect, but since we're all sharing.
For the hight level concept you may consider using a pull rather then a push principle by introducing a concept of streams on top of your TCP/UDP 'connection'. You still need some sort of protocol on the streams but the message routing is done with a higher abstraction - it comes down to writing to a buffer based on a numerical ID - and you may also use configurable reliabilities if you run on UDP. On the receiving end the responsible subsystem actively pulls messages out of its respective stream and with a concept of a stream reader that can be done on a higher level and the semantics of a specific channel are well hidden from the transport layer. Acknowledges for channels with no or sequence only reliability can be inserted into the master stream as sort of fences (the stream readers can take care of sending acks were necessary) and for ordered channels your transport layer takes care of it.

This topic is closed to new replies.

Advertisement