Advertisement

Packet Protocol/layout

Started by November 16, 2005 04:51 AM
15 comments, last by _winterdyne_ 19 years, 3 months ago
I've been tarting around with a small MMO-style RPG game, which is actually aimed at perhaps 4-8 players (hence MMO-style, not MMO). Currently I send packets with a 0x0001 style character at the beginning, followed by a message like: "%c %d %f %f %f" - movement character (0x0003), character id, x, y, z I'm wondering if someone might be able to point me towards a decent way of handling/laying out my packets, as I'm having trouble sorting out which 0x0001 style numbers work as special characters, and obviously I'd like to eventually do a decent job on the networking code. Mucho thanks, -Bobo
The first 32 ANSI characters are control codes. But why not send a (hexa)decimal number instead of a single character?
If efficiency becomes a problem you can always switch to a binary protocol later on.
Advertisement
A good practice is to separate the concept of packet from 'message'.

If you switch to using a UDP based protocol, you can't guarantee that the entire message or any packet in it will arrive in order, or at all.

So your packet should be marked with a messageID(so you know what particular message this is) and a senderID(so you know which set of messages the ID is relevant to), and a messageOffset (to state how far into the message it is), and a packetLength (so you can tell if the entire packet has been received or whether it needs resending). Following that you stick your actual message.

When receiving a packet, keep track of its length - you might not receive a properly formed packet (incomplete header), or it might not have its entire payload (Received length less than specified packet length).

Packet{   PacketHeader   {       messageID       senderID       messageOffset       packetLength   }   payload[] (packetLength bytes)}


Your message also needs a particular format to allow it to be reconstructed successfully. Obviously, the messageID and senderID (to allow the individual message to be identified), the messageLength (to determine whether the whole thing is complete) and then the payload of the message. You could alternatively replace messageLength with messageType (many messages will have a precise length determined by the type of message) and only send length when required, but if you're aiming for a flexible reliable UDP, I'd stick with encoding the length.

Message{   MessageHeader   {      messageID      senderID      messageLength   }   payload[] (messageLength bytes)}


Hope this helps.
Winterdyne Solutions Ltd is recruiting - this thread for details!
Can someone please delete that triple post? :-( Dunno what happened there.
Winterdyne Solutions Ltd is recruiting - this thread for details!
Unfortunately thats the bit of structured packets that I do understand. I don't understand how to use them, or how to form them in a suitable way. A place with an example might be better.. I hate being simple :(.
Here's the code I use to send a message (actually place it into a send queue). This is a refactoring of code from Planeshift, for the network portion of my library. Notice that there is a priority included in the packet structure - this is to notify the recipient that an ACK should be sent.
What this is basically doing is decomposing a full message (of any length) into a number of packets that are placed in a queue for sending. The packet size should be altered depending on the network you'll be sending it over. I use 512 bytes maximum as a packet size. Shorter packets are acceptable, and are sent individually (no collecting of short packets). The packet queue is processed for a set time each server tick, with the entire area of a packet object being transmitted (including the header).

bool WSNetBase::sendMessage(WSNetMsgEntry* pMsg,WSNetPacketQueueRefCount* pQueue){	if (!pQueue)		pQueue = m_pNetworkQueue;	// Default queue for comms	unsigned int iTotalSize = pMsg->m_pMessage->getTotalSize();	unsigned int iBytesLeft = iTotalSize;	unsigned int iOffset = 0;	unsigned int iMessageID = getRandomID();	unsigned int iPacketLen = 0;	WSPacketEntry* pPacketEntry;	while (iBytesLeft>0)	{		iPacketLen = iBytesLeft;		if ( iPacketLen > m_iMaxPacketData )			iPacketLen = m_iMaxPacketData;		pPacketEntry = new WSPacketEntry(			pMsg->m_cPriority,			pMsg->m_iNetObjectID,			iMessageID,			iOffset,			iTotalSize,			iPacketLen,			pMsg->m_pMessage);		if (!pQueue->add(pPacketEntry))		{			m_pLog->log("Error adding packet %x to queue %x - queue full.\n",pPacketEntry,pQueue);			delete pPacketEntry;	// Prevent obscene memory leak. Either planeshift has aggressive									// memory management or it leaks here. Looks more like it leaks.			return false;		}		iBytesLeft -= iPacketLen;		iOffset += iPacketLen;	}	return true;};


Packets are received from the socket layer and placed into queues. The packets in these queues may not be properly formed, may come from unknown senders, may not be in any particular order, but they will at least have a packet header (otherwise they wouldn't have made it as far as the queues).

This is the code used to build a message from the packet queues. This is triggered each and every time a packet is encountered in the queue. This will first insert the packet into a binary tree sorting packets by message ID and sender ID, then will call checkCompleteMessage() on encountering a message fragment to see if the entire message can be built. If the message is built, it is sent to a handler, otherwise the packets are left in the tree for future use.


bool WSNetBase::buildMessage(WSPacketEntry* pPacketEntry,WSNetConnection* pConn,WSNetAddress& rAddr){	// We've received a packet that should form part of a message.	// It will either be a complete message in one packet, or a partial fragment.		if (pConn)	{		// Getting a packet is enough to keep the connection live		pConn->m_iHeartbeat = 0;		pConn->m_iLastRcvTime = Timer.getMilliSeconds();	}	WSPacket* pPacket = pPacketEntry->m_pPacket;	WSNetMsgEntry* pMsgEntry = 0;	// Is this a whole message?	if (pPacket->m_iMsgOffset == 0 && pPacket->m_iPacketSize == pPacket->m_iMsgSize)	{		// This appears to be a complete message.		if (pPacket->m_iPacketSize < sizeof(WSNetMessage))		{			// Packet too short for message header - this is a bad packet.			m_pLog->log("Dropping packet too short for message header.\n");			return false;		}		WSNetMessage* pMsg;		pMsg = (WSNetMessage*) pPacket->m_Data;		// The packets report of the message size should agree with the message header		if (pPacket->m_iMsgSize != pMsg->getTotalSize())		{			// Disagreement between packet and message headers			m_pLog->log("Dropping packet with inconsistent message header.\n");			return false;		}		else		{			pMsgEntry = new WSNetMsgEntry(pMsg);			pMsgEntry->m_cPriority = pPacket->getPriority();			m_pLog->log("Single packet message received.\n");			handleCompletedMessage(pMsgEntry,pConn,rAddr,pPacketEntry);		}		return false;	}	// This appears to be a fragment	m_Fragments.Insert(pPacketEntry);	// Insert this packet entry into the fragments list.	// checkCompleteMessage returns a message entry if the message could be built and ALSO	// deletes packets if it manages to build the message.	pMsgEntry = checkCompleteMessage(pPacketEntry->m_iNetObjectID,pPacket->m_iMsgID);	if (pMsgEntry)	{		m_pLog->log("Multiple packet message received and rebuilt.\n");		handleCompletedMessage(pMsgEntry,pConn,rAddr,pPacketEntry);		return true;	// The message is complete; packets were deleted by checkCompleteMessage	}	else	{		m_pLog->log("Message fragment received.\n");		return true;	// Message is being built. Do NOT delete packet entry.	}};


This function runs through a binary tree of message fragments, attempting to complete the message from the fragments in the binary tree. If successful, it returns a message entry (wrapper for a message) which is then passed to a handler.

// This function attempts to build the message (ID) associated with (iNetObjectID).// If it can, it returns a MsgEntry structure.WSNetMsgEntry* WSNetBase::checkCompleteMessage(unsigned int iNetObjectID,unsigned int iMsgID){	// Planeshift allocates a new packet entry each time for this.	// We keep a pet one for comparisons in this class		// These are the members that packet entries are compared on (operator ==())	m_pCompPacketEntry->m_iNetObjectID = iNetObjectID;	m_pCompPacketEntry->m_pPacket->m_iMsgID = iMsgID;		// This is common for all packets with this message	m_pCompPacketEntry->m_pPacket->m_iMsgOffset = 0;		// This is the offset into the message for the packet		WSPacketEntry* pPacketEntry;	pPacketEntry = m_Fragments.Find(m_pCompPacketEntry);	if (!pPacketEntry)	{		// No packet with offset 0 found for the message iMsgID		m_pLog->log("No first packet found for message %d. Packets probably arriving out of order.\n",iMsgID);		return 0;	}	// Otherwise we have a first packet.		unsigned int iLength = 0;	unsigned int iTotalLength = pPacketEntry->m_pPacket->m_iMsgSize;	bool bInvalidated = false;		WSPacket* pPacket = pPacketEntry->m_pPacket;	WSNetMsgEntry* pMsgEntry;	// The message entry ctor allocates a buffer for the message to go in	// If we fail to build the message we MUST DELETE THIS MSG ENTRY	pMsgEntry = new WSNetMsgEntry(iTotalLength,pPacket->getPriority());	BinaryRBIterator<WSPacketEntry> loop(&m_Fragments);	// We reuse pPacketEntry and pPacket in this loop.	// The tree is in:		// netObjectID (ascending)			// packet (message) ID (ascending)				// offset (ascending)	for (pPacketEntry = loop.First(); pPacketEntry; pPacketEntry = ++loop)	{		if (pPacketEntry->m_iNetObjectID < iNetObjectID)	// This is not the object we're interested in			continue;		if (pPacketEntry->m_iNetObjectID == iNetObjectID) // This IS the object we're interested in		{			pPacket = pPacketEntry->m_pPacket;	// We need to examine packets in detail. Avoid multiple indirection.			if (pPacket->m_iMsgID < iMsgID)	// These packets do not comprise the message we're interested in.				continue;			if (pPacket->m_iMsgID == iMsgID) // These ARE the packets we're interested in			{							// Verify fragment reports same message size				if (iTotalLength != pPacket->m_iMsgSize)				{					// If we're here the message fragments have inconsistent size.					// This could be down to someone sending invalid fragments to try to cause					// a crash, or could be down to simple bad luck from ID generation. In any case					// we'll invalidate and ignore the message.					// This can be avoided by having the ID generation check for ID's already in use.					bInvalidated = true;					m_pLog->log("Discarding message ID %d from object %d. Message fragments have inconsistent total sizes.\n",iMsgID,iNetObjectID);					break;				}				// Verify that packet will not overflow the message buffer.				// This should not happen unless someone has altered the client.				if (pPacket->m_iMsgOffset + pPacket->m_iPacketSize > iTotalLength)				{					// If we're here the packet we're examining would overflow the message					// buffer. This is unlikely to happen unless the client has been altered.					bInvalidated = true;					m_pLog->log("Discarding message ID %d from object %d. A message fragment associated with this message would overflow the buffer.\n",iMsgID,iNetObjectID);					break;				}				// The fragment appears to be ok. Copy it into the awaiting message entry.				memcpy(					(pMsgEntry->m_pMessage) + pPacket->m_iMsgOffset,	// Target					pPacket->m_Data,									// Source					pPacket->m_iPacketSize);							// Length				iLength += pPacket->m_iPacketSize;			}			else if (pPacket->m_iMsgID > iMsgID)			{				break;	// We're now past the packets we're interested in.			}		}		else		{			break;	// Discontinue loop as we're past the object we're interested in.		}	}	// We now have a WSNetMsgEntry object which may only be partially complete, or	// may be invalidated.	// We can exit early if we don't have a cleanup job to do - i.e. we haven't finished	// receiving the message and it doesn't look invalid yet.	if (!bInvalidated && iLength != iTotalLength)	{		delete pMsgEntry;		return 0;	}	// The entire message has either been read or found to be invalid. In either case	// we need to remove its packets from the Fragments tree.	// We again use the member packet entry here for searching, since it saves allocator	// time and is already set up with the correct iNetObjectID and iMsgID.	while ((m_pCompPacketEntry->m_pPacket->m_iMsgOffset < iTotalLength) && 		   (pPacketEntry = m_Fragments.Find(m_pCompPacketEntry)))	{		m_pCompPacketEntry->m_pPacket->m_iMsgOffset += pPacketEntry->m_pPacket->m_iPacketSize;		while (m_Fragments.Delete(pPacketEntry)>0);	// Remove from fragment tree		delete pPacketEntry;						// Delete packet.	}	// Ensure that our search finished at the end of the message.	if (!bInvalidated && (m_pCompPacketEntry->m_pPacket->m_iMsgOffset != iTotalLength))	{		// The fragments do not occupy the whole message. This is bad.		bInvalidated = true;		m_pLog->log("Discarding message %d from object %d. Fragments leave gaps in message -  ### POSSIBLE HACK! ###\n",iMsgID,iNetObjectID);	}	if (bInvalidated)	{		delete pMsgEntry;	// Clean up		return 0;			// return	}	// The message entry is complete and has no gaps.	return pMsgEntry;};


The message format itself depends entirely on the type of message - I send a message type byte which is used to determine what the message is, and what should be done with the rest of the payload - I'd assign a type to each 'verb' in your game, and use the rest of the payload for arguments. Which is pretty much what you are doing from your OP.
Winterdyne Solutions Ltd is recruiting - this thread for details!
Advertisement
Holy crap, thats a big bit of code compared to what I do. Thanks very much, hopefully I'll be awake enough somewhen over the next few days to be able to understand what it's on about.
No worries, any questions just pm and I'll try to answer as best I can.
Winterdyne Solutions Ltd is recruiting - this thread for details!
Quote:
Original post by _winterdyne_and a packetLength (so you can tell if the entire packet has been received or whether it needs resending). Following that you stick your actual message.

Well, UDP does guarantee that packets are received correctly *if* they're received. You won't get partial packets, just like you won't get corrupted ones.
That's about all UDP guarantees though... [lol]
senderID isn't necessary because the sender can be inferred from the ip:port returned by recvfrom (for UDP) or from the socket itself (for TCP).

I also try to avoid variable-length messages as much as possible so I very rarely find the need for a length field (the length is derived from the message id). The times I've had to work on existing systems that used embedded message lengths extensively it usually ended up being just another field that needed extensive verification and couldn't really be used as anything more than a hint.
-Mike

This topic is closed to new replies.

Advertisement