Packet data types

Started by
25 comments, last by rip-off 12 years, 10 months ago
Hello,

I have been reading all over about network game programming and they all seem to be somewhat concerned about being able to re-send a packet if it was lost. I don't understand why you would want to be able to resend a packet when your running a real time game. Am I missing something here?

My more important question is regarding sending out different data types. At the moment the network part of my game is insignificant. It only sends out a struct on the loopback address containing data such as the sequence, ack and position information of my one 3d model. What I want to know is what if I want to send out a stuct that contains slightly different information and data types? At the moment my game simply receives data into an identical struct. I can't imagine its efficient to have a struct that contains every bit of data that you may ever need. I hope this is not an obvious answer but i have tried to figure this out and look it up with no luck.

Thanks for your help.

agisler
Advertisement
You've got the right idea (except that you want to use some proper serialization instead of a struct--I like POSH).

However, you're going to eventually have data that HAS to get there and be in the proper order (login authorization, chat messages, trade, etc). You don't want to be resending huge chat messages over and over until you get an ack.

Just let enet handle this for you, it's the cleanest simplest UDP libarary I've seen.
Anthony Umfer
Thanks for the help,
I can't believe I got the right idea. Seems too simple :)

I have read the info regarding posh on there website, however its a bit over my head. Could you please explain to me what it does in a quick dumb downed version :) From what I gather its basically a custom data type? would this be an accurate statement?

This September I will be in my final year of a university studying computer networks. The networking part of the game I am building is what my final year project will be. I want to write my own code for this reason. It will not have any chatting or etc.. All I want to accomplish is a very basic flow control with some reliability in the areas I need.However I do plan on using ENET for my backup if all else fails ;)

Thanks for the help.

agisler

I have read the info regarding posh on there website, however its a bit over my head.


I've never used that library, but all it seems to do is to convert between big- and little-endian elements in memory, plus define some pre-defined constants based on what computer you're building it for. You can do almost all of this with the simple functions htons() and htonl() that are built into the sockets library. (except for the 64-bit versions)

In general, serialization means that you turn a high-level data structure into a low-level stream of bytes (kind of like saving to a file), and then back again. The stream of bytes should be self-contained given the known state of the serializer, but, for games, doesn't generally need to be self-explanatory. Thus, XML (where you name each field explicitly) is probably too verbose -- in a game, you know that when you see a "player position" packet, you will see (perhaps) a short for the player id, followed by three floats for player X, Y and Z.

Finally, you should separate "messages" from "packets." You send messages between endpoints in your system. The underlying network uses packets to transfer data from one node to another. Generally, you want to put many messages into a single packet, so as to minimize the overhead of packet headers. Many games will schedule a packet to be sent 1, 5, 10, or 30 times per second, and will have a queue of messages, where the queue will be put into a packet when the time comes, and all those messages sent at once.
To make this un-packable on the other side, you typically want each message to be encoded with a message type, and perhaps a length field, so that it's easy to tell messages apart and dispatch each of them separately.
enum Bool { True, False, FileNotFound };
Typically, what I've done for exchanging data, is have a typical Packet header structure, which contains data values that every packet would need, and i include payload for packet type specific data.

For example, in my MUNE networking engine (http://mune.will.jennings.name) , I had a packet header structure like this:

[source]

typedef struct
{
U16 uwType;
U16 uwToUsers;
U16 uwFromUser;
U16 uwExtra;
U32 ulSize;
char pcData[1];
} tMunePacket;
[/source]

So, every message would be parsed based on it's type. If the message type required more data, it would be stored in the pcData. For example, if the message type was PLAYER_STATUS_UPDATE, the data in pcData may be a structure which has position and player status info. you could read and write it like this:

[source]

typedef struct
{
U32 u32PlayerId;
float fXpos;
float fYpos;
U32 u32PlayerHealth;

} tPlayerStatus;

tPlayerStatus gMainPlayerStatus;

.
.
.
tMunePacket *pPacket;

/* allocate room for header and data */
pPacket = (tMunePacket *)malloc(sizeof(tMunePacket ) + sizeof (tPlayerStatus));

/* fill in pPacket header data */
pPacket->uwType = PLAYER_STATUS_UPDATE;
/* copy player data in payload */
memcpy(pPacket->pcData, &gMainPlayerStatus, sizeof(tPlayerStatus));
[/source]

to read it out, you'd do this:
[source]

tPlayerStatus CurrentPlayerStatus;

switch (pPacket->u16MsgType)
{
case PLAYER_STATUS_UPDATE:
/* read player data out */
memcpy(&CurrentPlayerStatus, pPacket->pcData, sizeof(tPlayerStatus );
break;
}

[/source]

Just an example of how I've done it in the past.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

I really think you're better off defining packets as functions as opposed to types. (Well, functions are better than types in general so :P)

1. Endianness
2. Padding
3. Flexibility

Store the data wherever it is used by the game code, then write it to the packet in a function.


void PlayerStatus(Packet& packet, const Player& player) {
packet.WriteU16(player.id);
// etc
}


Your Packet class would store an internal byte array, and provide methods to read/write exact data types. RakNet has a decent implementation.
Anthony Umfer


void PlayerStatus(Packet& packet, const Player& player) {
packet.WriteU16(player.id);
// etc
}



Actually, if you want to go that way, you probably want to declare packets (or serialization) as visitors.


struct MyStruct {
int someId;
std::string someString;
float someValue;
};

template<typename Stream> Stream &visit(MyStruct &p, Stream &strm)
{
return strm.visit("someId", p.someId)
.visit("someString", p.someString)
.visit("someValue", p.someValue);
}


Now, you can have one stream class that reads a struct and writes a byte stream, and another stream class that reads a byte stream and visits a struct.


class InStream {
public:
std::vector<unsigned char> buf;
InStream &visit(char const *name, int const &i) {
buf.push_back((i >> 24) & 0xff);
buf.push_back((i >> 16) & 0xff);
buf.push_back((i >> 8) & 0xff);
buf.push_back(i & 0xff);
return *this;
}
InStream &visit(char const *name, std::string const &str) {
if (str.size() > 255) throw std::exception("too long string");
buf.push_back(str.size());
buf.insert(buf.end(), str.begin(), str.end());
return *this;
}
...


The reason you pass the name of the field in as well as the field reference is that you may want to also marshal to JSON, XML, or an editor UI of some sort, which will need the name.
enum Bool { True, False, FileNotFound };
Ok you guys are blowing my head away lol I thought this was a simple question that would have a simple answer :P I was wrong.

I like your suggestion BeerNutts, mostly cause its the one I can understand :P Just checking if I understand you correctly, all your basically doing is copying the pcData into a char? Of which the char is inside a struct that defines what type of packet it is. Is that about right?

As for the serialization this is quite difficult for me to understand at this point. Because of my lack of understanding I am struggling with the code and the benefits of using this method.


[color="#1C2837"]
Actually, if you want to go that way, you probably want to declare packets (or serialization) as visitors.

[color="#000088"]struct [color="#660066"]MyStruct [color="#666600"]{ [color="#000088"]int[color="#000000"] someId[color="#666600"];[color="#000000"]
std[color="#666600"]::[color="#000088"]string[color="#000000"] someString[color="#666600"]; [color="#000088"]float[color="#000000"] someValue[color="#666600"]; [color="#666600"]}; [color="#000088"]template[color="#666600"]<[color="#000088"]typename [color="#660066"]Stream[color="#666600"]> [color="#660066"]Stream [color="#666600"]&[color="#000000"]visit[color="#666600"]([color="#660066"]MyStruct [color="#666600"]&[color="#000000"]p[color="#666600"], [color="#660066"]Stream [color="#666600"]&[color="#000000"]strm[color="#666600"]) [color="#666600"]{ [color="#000088"]return[color="#000000"] strm[color="#666600"].[color="#000000"]visit[color="#666600"]([color="#008800"]"someId"[color="#666600"],[color="#000000"] p[color="#666600"].[color="#000000"]someId[color="#666600"]) [color="#666600"].[color="#000000"]visit[color="#666600"]([color="#008800"]"someString"[color="#666600"],[color="#000000"] p[color="#666600"].[color="#000000"]someString[color="#666600"]) [color="#666600"].[color="#000000"]visit[color="#666600"]([color="#008800"]"someValue"[color="#666600"],[color="#000000"] p[color="#666600"].[color="#000000"]someValue[color="#666600"]); [color="#666600"]}

Now, you can have one stream class that reads a struct and writes a byte stream, and another stream class that reads a byte stream and visits a struct.

[color="#000088"]class [color="#660066"]InStream [color="#666600"]{ [color="#000088"]public[color="#666600"]:[color="#000000"]
std[color="#666600"]::[color="#000000"]vector[color="#666600"]<[color="#000088"]unsigned [color="#000088"]char[color="#666600"]>[color="#000000"] buf[color="#666600"]; [color="#660066"]InStream [color="#666600"]&[color="#000000"]visit[color="#666600"]([color="#000088"]char [color="#000088"]const [color="#666600"]*[color="#000000"]name[color="#666600"], [color="#000088"]int [color="#000088"]const [color="#666600"]&[color="#000000"]i[color="#666600"]) [color="#666600"]{[color="#000000"]
buf[color="#666600"].[color="#000000"]push_back[color="#666600"](([color="#000000"]i [color="#666600"]>> [color="#006666"]24[color="#666600"]) [color="#666600"]& [color="#006666"]0xff[color="#666600"]);[color="#000000"]
buf[color="#666600"].[color="#000000"]push_back[color="#666600"](([color="#000000"]i [color="#666600"]>> [color="#006666"]16[color="#666600"]) [color="#666600"]& [color="#006666"]0xff[color="#666600"]);[color="#000000"]
buf[color="#666600"].[color="#000000"]push_back[color="#666600"](([color="#000000"]i [color="#666600"]>> [color="#006666"]8[color="#666600"]) [color="#666600"]& [color="#006666"]0xff[color="#666600"]);[color="#000000"]
buf[color="#666600"].[color="#000000"]push_back[color="#666600"]([color="#000000"]i [color="#666600"]& [color="#006666"]0xff[color="#666600"]); [color="#000088"]return [color="#666600"]*[color="#000088"]this[color="#666600"]; [color="#666600"]} [color="#660066"]InStream [color="#666600"]&[color="#000000"]visit[color="#666600"]([color="#000088"]char [color="#000088"]const [color="#666600"]*[color="#000000"]name[color="#666600"],[color="#000000"] std[color="#666600"]::[color="#000088"]string [color="#000088"]const [color="#666600"]&[color="#000000"]str[color="#666600"]) [color="#666600"]{ [color="#000088"]if [color="#666600"]([color="#000000"]str[color="#666600"].[color="#000000"]size[color="#666600"]() [color="#666600"]> [color="#006666"]255[color="#666600"]) [color="#000088"]throw[color="#000000"] std[color="#666600"]::[color="#000000"]exception[color="#666600"]([color="#008800"]"too long string"[color="#666600"]);[color="#000000"]
buf[color="#666600"].[color="#000000"]push_back[color="#666600"]([color="#000000"]str[color="#666600"].[color="#000000"]size[color="#666600"]());[color="#000000"]
buf[color="#666600"].[color="#000000"]insert[color="#666600"]([color="#000000"]buf[color="#666600"].[color="#000088"]end[color="#666600"](),[color="#000000"] str[color="#666600"].[color="#000088"]begin[color="#666600"](),[color="#000000"] str[color="#666600"].[color="#000088"]end[color="#666600"]()); [color="#000088"]return [color="#666600"]*[color="#000088"]this[color="#666600"]; [color="#666600"]} [color="#666600"]...[/quote]

From what I can understand your saying I have a class that reads a structure when the data comes in and then outputs the data to a file essentially? Then you have a class that does the reverse of that?

This is a bit over my head but I do not have a problem reading about it and trying to learn this. However what's the benefits of me doing it this way?


Once again thank you all for the help

agisler
[font=arial, verdana, tahoma, sans-serif][size=2]

Ok you guys are blowing my head away lol I thought this was a simple question that would have a simple answer :P I was wrong.

I like your suggestion BeerNutts, mostly cause its the one I can understand :P Just checking if I understand you correctly, all your basically doing is copying the pcData into a char? Of which the char is inside a struct that defines what type of packet it is. Is that about right?



Well, it's kind of a trick. The structure's last item is char pcData[1];

Technically, this defines an array of chars of length 1; however, when I allocate the memory for the structure, I have size for the structure, plus size for the "payload" data. What this actually does is points pcData to allocated memory the size of the payload structure. It allows you to pass the packet structure with the payload data on the end, which is how I handle serialization in C. So, I can memcpy any structure into pcData, send the full packet structure , plus the payload length, to the server. When the client gets the data, he reads the packet header, determines the type, and can copy the payload data from pcData into whatever structure type he wants.

In my example above, I was copying the payload data in the tPlayerStatus structure, which defines a certain players status.

This requires some working knowledge of pointers and memory allocation. That's why, in my MUNE engine, I only provide a function to define packets and append data for you:

tMunePacket * MuneCreatePacket (U16 uwType, U16 uwTo, U16 uwFrom, U8 *pucData, U32 ulSize)


(you can get the source code from mune.will.jennings.name as well if you want)

Realize, all my code is in C. The other guys are offering options in C++ if you want to go that route.
[/font]

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)



As for the serialization this is quite difficult for me to understand at this point. Because of my lack of understanding I am struggling with the code and the benefits of using this method.

...


From what I can understand your saying I have a class that reads a structure when the data comes in and then outputs the data to a file essentially? Then you have a class that does the reverse of that?


You may be better off doing more programming on a single machine, until you are more at ease with memory, bytes, how data structures are represented, and other such systems programming details.

Anyway, yes, a TCP stream of packets, and a file on disk, are actually very much similar. What my proposed marshaling does is define a particular struct per message type, and then build *one* template function per struct. All this function does is visit each struct member in order, calling into some template argument for each. You can then provide two classes: an "input reader" and an "output writer," which lets you read into the struct, or write out of the struct, depending on which you pass in.

What's extra elegant about this is that you can also write a class that does things like "build a GUI" or "send to log file" or whatever -- when you have the struct, and the visitor function, you can do many things to any instance of that struct, by simply building a different "stream" class, and using the same visitor. This allows you to separate the concerns of what the data is (the struct), how to describe what the data is (the visitor function), and what you do with a description of the data (the different class implementations).

This means you don't have to write separate "send()" and "receive()" and "editInGUI()" and "dumpAsXML()" and "logToFile()" functions for the same data struct, which is a pretty big win once your program becomes bigger.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement