Sign in to follow this  
nto

Handling network packages

Recommended Posts

nto    122
Hey everyone! I've begun implementing multiplayer in a 2d shooter game with flexible terrain. I've been trying to think of the best way to design the network engine. Currently what I've come up with is sending network packages of the following form: [header] type: 1 byte length: 2 bytes [body] content: length bytes An example could be I store some movement data in the content and set the type to movement and length accordingly. I've made the following class for storing this information:
class netpackage
{
public:
	netpackage();
	netpackage(const netpackage& rhs);
	~netpackage();

	const netpackage& operator=(const netpackage& rhs);

	Uint16 getType() {return m_type;}
	void setType(Uint8 type) {m_type = type;}

	Uint16 getLength() {return m_length;}
	void setLength(Uint16 length) {m_length = length;}
	
	void* getData() {return m_data;}
	void setData(void* data) {m_data = data;}	

protected:
	Uint8 m_type;
	Uint16 m_length;
	void* m_data;
};
But I'm betting there's a smart way of doing this using inheritance or something. I'm open to all suggestions on how to design my network packages. When I have to convert the content to something usable again, I'm not sure what the best approach is. I tried finding information about this in the forum FAQ and there's something described about this, but I'm wondering what a good way to design the general network package is, so I have a flexible network engine that's easy to add new package types to. Thanks in advance, nto.

Share this post


Link to post
Share on other sites
Antheus    2409
class Serializable {
virtual void write( Buffer &b ) = 0;
};

class Buffer
{
enum { MAX_SIZE = 1024 };

char buffer[MAX_SIZE]
int length;
int index;
};

template < class T >
Buffer &operator<<( Buffer &buffer, T t)
{
if ( (buffer.index + sizeof(T)) > buffer.MAX_SIZE) // error, overflow

memcpy( &buffer[buffer.index], &t, sizeof(T) );
buffer.index += sizeof(T);
buffer.length += sizeof(T);
return buffer;
}

Buffer &operator<<( Buffer &buffer, Serializable &t)
{
t.serialize( buffer );
return buffer;
}

...

class MovementPacket : public Serializable
{
virtual void write( Buffer &buffer )
{
buffer << x << y << z;
}

float x;
float y;
float z;
};

...


Buffer b;
MovementPacket m;
b << m;

send( b.buffer, b.length );




The basic idea...

All the objects you want to serialize need to either implement Serializable (intrusive way, works for your classes), or you must provide the proper << and >> operators (for classes you can't change, such as std::string). Every non-primitive type must define one of these - strings as well.

Share this post


Link to post
Share on other sites
nto    122
Thanks for the reply! This definitely looks like something I'd be able to use! I have one question though, how would it look when I need to unserialize the package again? How do I know what type of package it is?

/nto

Share this post


Link to post
Share on other sites
hplus0603    11347
Typically, when you send the packet, you pre-fix it with a packet type code. That can, in turn, be templated. Something like:


template<typename T> struct TypeCodes;
template<> struct TypeCodes<LoginPacket> {
enum { Code = 1; }
};
...
class Sender {
...
template<typename T> void Send(T const &t) {
Block b;
b << TypeCodes<T>::Code;
b << t;
socket.send(b.data(), b.size());
}
};



With the right amount of template trickery and macros, you can make both packet creation and packet dispatch (the fan-out on the receiving side) be fairly easy to express, and robust in that you don't need to change a lot of places when you add or change a packet.

Share this post


Link to post
Share on other sites
nto    122
Alright, I think I got it now. Thanks a lot, both of you :)

I really like the flexibility of this solution :)

Share this post


Link to post
Share on other sites
nto    122
I have a quick question about the first method that Antheus posted. Here is the relevant code:


In netbuffer.h
#ifndef NETBUFFER_H_
#define NETBUFFER_H_

#include <SDL/SDL_net.h>
#include <iostream>

class serializeable;

class netbuffer
{
public:
netbuffer();
netbuffer(const netbuffer& rhs);
~netbuffer();

const netbuffer& operator=(const netbuffer& rhs);
netbuffer& operator<<(serializeable &t);

template < class T >
netbuffer& operator<<(T t);

Uint8* getBuffer() {return m_buffer;}
Sint32 getIndex() {return m_index;}
void setIndex(Sint32 index) {m_index = index;}
Sint32 getLength() {return m_length;}
void setLength(Sint32 length) {m_length = length;}

Uint16 getPacketLength();
Uint8 getPacketType();

void write(void* data, Sint32 length);

class exception_overflow {};

protected:
enum { MAX_SIZE = 1024 };

Uint8 m_buffer[MAX_SIZE];
Sint32 m_index;
Sint32 m_length;
};

class serializeable {
public:
virtual void serialize( netbuffer &b ) = 0;
virtual ~serializeable() {};
};

template < class T >
netbuffer& netbuffer::operator<<(T t)
{
if ( (m_index + sizeof(T)) > MAX_SIZE) // error, overflow
throw exception_overflow();

memcpy(&(m_buffer[m_index]), &t, sizeof(T));
m_index += sizeof(T);
m_length += sizeof(T);
return *this;
}

#endif





In netbuffer.cpp

#include "netbuffer.h"

#include <iostream>

netbuffer::netbuffer()
{
m_index = 0;
m_length = 0;

memset(m_buffer, 0, MAX_SIZE);
}

netbuffer::~netbuffer()
{

}

netbuffer::netbuffer(const netbuffer& rhs)
{
if (this != &rhs)
{
m_index = rhs.m_index;
m_length = rhs.m_length;

memset(m_buffer, 0, MAX_SIZE);
memcpy(m_buffer, rhs.m_buffer, rhs.m_length);
}
}

const netbuffer& netbuffer::operator=(const netbuffer& rhs)
{
if (this != &rhs)
{
m_index = rhs.m_index;
m_length = rhs.m_length;

memset(m_buffer, 0, MAX_SIZE);
memcpy(m_buffer, rhs.m_buffer, rhs.m_length);
}

return *this;
}

netbuffer& netbuffer::operator<<(serializeable &t)
{
t.serialize( *this );
return *this;
}

void netbuffer::write(void* data, Sint32 length)
{
if ( (m_index + length) > MAX_SIZE) // error, overflow
throw exception_overflow();

memcpy(&(m_buffer[m_index]), data, length);
m_index += length;
m_length += length;
}

Uint16 netbuffer::getPacketLength()
{
if (m_length >= 3)
{
Uint16* length = (Uint16*)&(m_buffer[1]);
return *length;
}
else
return 0;
}

Uint8 netbuffer::getPacketType()
{
if (m_length > 0)
return m_buffer[0];
}





In main.cpp:

#include <iostream>
#include "netbuffer.h"

class randomclass : public serializeable
{
public:
void serialize(netbuffer& b) {std::cout << "Why doesn't this get called?" << std::endl;};
protected:
int x;
int y;
};

int main(int argc, char **argv)
{
Uint8 l = 100;
Uint16 lala = 13;

randomclass rc;
test << rc; //why does this call the template overload of << rather than the one with serializeable as argument?

test << l << lala;
std::cout << "type: " << (int)test.getPacketType() << " length: " << test.getPacketLength() << std::endl;
}



My question is why doesn't the overloaded function with serializeable as argument get called, when I use a class that derives from that object? (see the randomclass in the source). If I disable the templated overload of << it will call the correct one. So apparently somehow the program decides that calling the templated overload << is more appropriate, even when the object is derived from a serializeable class. I'm at a loss here and will appreciate any input.

Thanks in advance,
nto.

Share this post


Link to post
Share on other sites
hplus0603    11347
That's the rule of template overloading in C++. Matching for specializations is quite picky.

If you want the default to be that you treat the object as Serializable, then you can make a default template implementation with that assumption:


template<typename T> netbuffer& operator<<(T const &arg) {
return this->operator <<(static_cast<Serializable const &>(arg));
}


The static_cast<> will generate a compile error for any case where you don't have an explicit specialization for the type in question, and the type doesn't implement Serializable.

Share this post


Link to post
Share on other sites
nto    122
Thanks for your reply!

I got an ambiguous problem when using the above code, since both my << overloads were candidates. I found an alternative, although it requires a bit more work, it gets the job done. I'm just gonna use the templated version with a write function instead, so that I can pretty much choose. This isn't very automatic and requires me to specify how to serialize every packet, but for this game I'm gonna settle with this. Next time I do another game, I'll try and buy a book and brush up on my templates :-)

/nto

Share this post


Link to post
Share on other sites
Antheus    2409
Since that was an example, it didn't cover the specializations adequately.

The proper and safe form would be this:

class buffer {
template < class T >
buffer & write_raw( T value, size_t n ) {
memcpy( buf, &value, n );
}
}


buffer &operator<<( buffer& b, char i) {
return b.write_raw( i, 1 );
}
buffer &operator<<( buffer& b, int16 i) {
return b.write_raw( i, 2 );
}
buffer &operator<<( buffer& b, int32 i) {
...
}
buffer &operator<<( buffer& b, int64 i) {
...
}
buffer &operator<<( buffer& b, float i);
buffer &operator<<( buffer& b, std::string s)
{
b << s.size();
// write characters
}

...

template < class T >
void write( buffer, T &value ) {
value.write( buffer);
}

template < class T >
buffer &operator<<( buffer &b, T &value )
{
write( b, value );
return b;
}



Here, all the basic types you support are explicitly defined.
Anything that is not basic type, needs to either:
- Extend serializable interface and provide write and read methods
- Provide void write() and void read() free functions with their specific type

So adding type Point could be done like this:

class Point : public Serializable {
void write( buffer &b ) {
b << x << y << z;
}
};

// or

void write( buffer &b, Point &p ) {
b << p.x << p.y << p.z;
}


But any serialization that requires raw bytes to be written should be specialized, not templated (for portability and compatibility reasons you shouldn't rely on compiler defined sizes, but enforce them yourself).

Share this post


Link to post
Share on other sites
nto    122
Thanks for staying with me on this thread, Antheus. I'm gonna try and use what you suggest, but I think I'm gonna go brush up on my template knowledge a bit before making the final implementation. I really like this implementation of network packages, even if templates have been cruel to me :p

Thanks for your input!

/nto

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