Sign in to follow this  
Moe

Lidgren - packing data into packets...

Recommended Posts

So I've been playing around with Lidgren the past few days. I have to admit, this is the first time I've done any real network programming on my own. Lidgren seems almost dead simple to use, but there is still something that is bothering me. What I've been trying to do is create a system so I can easily differentiate the contents of a NetBuffer without having to read the entire thing. From the client I want to be able to send different messages. For example, when a client first connects, I want it to send a string containing the player's name. After that, I want to be able to send messages from the client either containing information saying that the player has moved/shot, etc, or text messages from the player to the other players on the server. My original thought on how to do this was to create a sort of generic class (for a lack of a better term) that would contain "encode" and "decode" methods. The encode/decode methods would be overridden by inheriting classes. Each of the inheriting classes would be for a different type of message, for example one would simply contain the player's name. Another message might contain a new player position (or direction of movement, etc). Another type of inherited message would contain text to be routed to all the other players. The problem I had with this is determining the type of the message without having yet created a new data structure of the specific type on the client side. The only way I could know what type the message was, was to read it in using the decode method on the base class, but I couldn't call that because I didn't know what type it was. In my second attempt at it, I created a class called "Message" that contained a Type (System.Type) and a copy of the first class that I had created in my first attempt. This way I could read the type out of the message, check what type it was, and create a new object based on the correct type and fill it in appropriately within each of the inherited classes overridden decode functions. I still couldn't get this to work for whatever reason (something not being initialized properly). Either way, I'm still quite curious to know how you guys have handled things like this. How do you distinguish between the different types of data being sent from the client?

Share this post


Link to post
Share on other sites
I've never really found a "good" way to do this. Currently, for sending, I just have a class full of static methods that pack the data and return the packed data, which I then send to whoever needs it. To unpack it, I have a separate method elsewhere.

For example, the server wants to create a user, so I call the public static method CreateUser(), and pass it the parameters it needs. It then returns a BitStream which I send to the map. The first few bits in this message is a ServerPacket.ID.CreateUser, where ServerPacket.ID is an enum.

On the client, I have an attribute that I attach to the method I want to handle the receiving of this message. So I'd have:


[MessageHandler(ServerPacket.ID.CreateUser)]
void RecvCreateUser(TCPSocket conn, BitStream r)
{
...
}




This method then always first reads the values, then tries to make use of them. This ensures that any error in trying to use the data, such as the data being invalid, doesn't result in me not reading everything first. The handling method is linked at runtime using reflection.

You could use structs to ensure consistency, and reflection to call the appropriate Write()/Read() methods to send every field, but this only works for very simple messages (ie "always writes all values"). I like to have a lot more control over my data, manually packing things, omitting values where possible, etc. Unfortunately, that puts you back at the step of having to manually update the encoder when you change the decoder, or vise versa, and can easily lead to problems.

Personally, I try to avoid making any objects at all when doing network I/O since the I/O calls add up very, very fast.

On a side note, hopefully my stupid, useless post gets someone like hplus or Antheus to come along and say, "Spodi, that way sucks, HERE is how it's done!". That would make my day, since I have spent a lot of time trying to improve this system, but can never find a good way. ;)

[Edited by - Spodi on November 13, 2008 4:44:17 PM]

Share this post


Link to post
Share on other sites
Quote:
Original post by Moe
Either way, I'm still quite curious to know how you guys have handled things like this. How do you distinguish between the different types of data being sent from the client?


This is usually done in a packet header. Each packet that you send contains a fixed length set of data that will tell you what you need to know of the data before you actually parse it. An example format would be:

Size Opcode CRC Payload
[00 00] [00 00] [00 00 00 00] [...]

In this case, you would read any received packet as:
size = msg.ReadUInt32(16);
opcode = msg.ReadUInt32(16);
crc = msg.ReadUInt32(32);


And the process the opcode to see what type of data you have.

public void Decode(NetMessage msg)
{
size = msg.ReadUInt32(16);
opcode = msg.ReadUInt32(16);
crc = msg.ReadUInt32(32);
switch(opcode)
{
// Name sent
case 1:
m_driverName = msg.ReadStringTable(msg.Sender);
break;

case 2:
m_occupied = msg.ReadBoolean();
m_gear = msg.ReadUInt32(3);
break;

default:
// Unhandled
}
}

The encode would work the same way, but in the opposite direction:

public NetMessage Encode(NetConnection conn, ... extra params ...)
{
NetMessage msg = new NetMessage();
msg.Write(size, 16);
msg.Write(opcode, 16);
msg.Write(crc, 16);

// pass msg Down to specific functions to build the packet based on whatever
// you are wanting to build
// Here's an example of how it'd look in one function

if(opcode == 1)
msg.WriteStringTable(conn, m_driverName);
else if(opcode == 2)
{
// Boolean uses just one bit of data
msg.Write(m_occupied);

// m_gear can only be 0 to 5, so 3 bits covers the entire range
msg.Write(m_gear, 3);
}

return msg;
}


Using this approach, you know the general type, and then can setup additional values to tell how to parse data in the packet itself. I.e.:
[Has Name] (Name if Has Name == 1) [Has Guild] (Guild if Has Guild == 1) ...
And valid packets might be:
01 'D' 'R' 'E' 'W' '\0' 00
00 00
01 'D' 'R' 'E' 'W' '\0' 01 'G' 'D' '\0'
00 01 'G' 'D' '\0'

Code based off of this doc page, but just written on the post, not actually tested. The idea should work fine for you though, most games I've seen take this approach. Good luck!

Share this post


Link to post
Share on other sites
Personally I use the method you first described with a header byte like Drew wrote.


class MyClass : IMessage
{
public MessageType { get { return MessageType.MyMessage; } }

void Encode(NetBuffer buffer)
{
// encode stuff specific for MyClass here
}

void Decode(NetBuffer buffer)
{
// decode stuff specific for MyClass here
}
}



... where MessageType is an enum (based on Byte) in the shared client/server code. This way the code for encoding and decoding is always close together, and it's a pretty good layout if you want to generate this code using reflection too. The message sending code is responsible for prepending the MessageType header byte when it's sent, and the receiving code collects all types implementing IMessage and holds a Dictionary for fast lookup MessageType => message class.

Share this post


Link to post
Share on other sites
Thanks for all the help guys. It has certainly given me more to think about!

I did end up getting things working the second way that I described (bundling the type and the data into once class, then reading out the type on the other end and casting it back into it's correct type on the other end). This does seem definitely less efficient compared to what Drew_Benton has suggested. I think I'll definitely have to continue to play around with this.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this