Handling network packages

Started by
8 comments, last by nto 16 years, 9 months ago
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.
Advertisement
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.
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
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.
enum Bool { True, False, FileNotFound };
Alright, I think I got it now. Thanks a lot, both of you :)

I really like the flexibility of this solution :)
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.
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.
enum Bool { True, False, FileNotFound };
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
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;  }};// orvoid 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).
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

This topic is closed to new replies.

Advertisement