Sign in to follow this  
Siberwulf

C# Message to Packet converter and assembler... Poke holes in it please :)

Recommended Posts

Siberwulf    163
First, lemme preface this by: "This is my first attempt at this, and I have almost no experience working with TCP/IP. Also, this is for TCP, so there are no Acks being sent back (however thats quite possible to implement, rather easily). All messages are sent as Low Priority right now, so they will automatically be wiped out of the buffer when sent. A quick class summary: GameCommand - a basic game command. Has various types {Movement, Social, Slash, Authentication, etc (for now)}, the text of the command, priority, and QueuePacket - A packet that is placed in a queue to be sent. There are 1 to 'n' packets for a GameCommand. CommandQueue - Basically everything having to do with sending or receive packets and the functions that involve building or dissecting them. These contain static arraylists that can be polled from the client and/or server. Here's a logical flow from a server command that executes and sets the client's ID, to be used in all subsequent transactions. No real authentication is in place yet. The server creates a command and then sends it to the player:
//Send them their new player ID
GameCommand objCmd = new GameCommand();
objCmd.Text = "SET_PID:" + ID.ToString();
objCmd.Type = GameCommand.CommandType.Authentication;
objPlayer.SendCommand(objCmd);

The player attaches its own ID to it, so it knows where to go:
public void SendCommand(GameCommand objCommand)
{
	//Tag this with the player ID
	objCommand.ObjectID = ID;

	//Nothing we can really do here, other than dump it to the connection's outbound Queue
	Conn.SendCommandToOutboundQueue(objCommand);
}

The connection then attaches its socket to the command. THis probably isn't necessary in UDP, but We're not into UDP, yet.
public void SendCommandToOutboundQueue(GameCommand objCommand)
{
	objCommand.DestSock = m_TCP;
	CommandQueue.SendToOutBoundQueue(objCommand);
}

The command is then picked up by the CommandQueue object and you can trace it below. Classes:
/// <summary>
/// "Teh!" game command
/// </summary>
public class GameCommand
{

	/// <summary>
	/// The type of command
	/// </summary>
	public CommandType Type;

	/// <summary>
	/// The priority of the current command
	/// </summary>
	public CommandPriority Priority;

	/// <summary>
	/// The actual text of this command
	/// </summary>
	public string Text; 

	/// <summary>
	/// The ID of the 'thing' that this command belongs to
	/// </summary>
	public int ObjectID;

	/// <summary>
	/// The destination socket of the command
	/// </summary>
	public Socket DestSock; 
	
	/// <summary>
	/// Constructor - default command priorty is low
	/// </summary>
	public GameCommand()
	{
		Priority = CommandPriority.Low;	
	}

	/// <summary>
	/// Creates a Command from a single packet
	/// </summary>
	/// <param name="objPacket">The packet containing the current command</param>
	public GameCommand(QueuePacket objPacket)
	{
		this.Type = objPacket.CommandType;
		this.Priority = objPacket.Priority;
		this.Text = objPacket.Message;
		this.ObjectID = (int)objPacket.PacketHeader.ObjectID;
	}

	/// <summary>
	/// Creates a command from a type and text
	/// </summary>
	/// <param name="CType"></param>
	/// <param name="Text"></param>
	public GameCommand(CommandType CType, string Text)
	{
		this.Type = CType;
		this.Text = Text;
		this.Priority = CommandPriority.Low;
	}

	/// <summary>
	/// Returns the text of the command in byte array.
	/// Prettified ;)
	/// </summary>
	/// <returns></returns>
	public byte[] GetBytes()
	{
		
		return System.Text.Encoding.UTF8.GetBytes(Text);
	}

	/// <summary>
	/// Types of command
	/// </summary>
	public enum CommandType
	{
		Movement,
		Social,
		Authentication,
		Slash

	}

	/// <summary>
	/// Priorty levels
	/// </summary>
	public enum CommandPriority
	{
		Low,
		High
	}
}

The Queue Packet Class -
/// <summary>
/// Sealed means its faster!
/// </summary>
public sealed class QueuePacket
{

	/// <summary>
	/// Max length of all packets
	/// </summary>
	public  const int MAXPACKETLENGTH = 17;

	/// <summary>
	/// Size of the header for a packet
	/// </summary>
	public const int HEADERSIZE = 7;

	/// <summary>
	/// Size of the actual data portion of the packet
	/// </summary>
	public const int MAXPACKETDATALENGTH = MAXPACKETLENGTH - HEADERSIZE;

	/// <summary>
	/// Priority of the packet
	/// </summary>
	public GameCommand.CommandPriority Priority;

	/// <summary>
	/// Type of packet
	/// </summary>
	public GameCommand.CommandType CommandType;

	/// <summary>
	/// The packet's header info
	/// </summary>
	public Header PacketHeader;

	/// <summary>
	/// Where this packet is headed to
	/// </summary>
	public Socket DestSock;

	/// <summary>
	/// Default bland-town constructor
	/// </summary>
	public QueuePacket()
	{
		PacketHeader = new Header();
	}

	/// <summary>
	/// The Message in this packet
	/// </summary>
	private byte[] m_Message;

	/// <summary>
	/// The string version of the packet message
	/// </summary>
	public string Message
	{
		set
		{
			m_Message = System.Text.Encoding.UTF8.GetBytes(value);
		}
		get
		{
			return System.Text.Encoding.UTF8.GetString(m_Message);
		}
	}

	/// <summary>
	/// Public property of the packet to return the bytes of the message
	/// </summary>
	public byte[] MessageBytes
	{
		get
		{
			return m_Message;
		}
	}

	/// <summary>
	/// What good is a packet if you can't send it?
	/// </summary>
	/// <returns></returns>
	public bool Send()
	{
		try
		{
			DestSock.Send(ToBytes());
			return true;
		}
		catch
		{
			return false;
		}
			
	}

	/// <summary>
	/// Converts current packet to byte array
	/// </summary>
	/// <returns></returns>

	private byte[] ToBytes()
	{
			
		byte[] objBytes = new byte[MAXPACKETLENGTH];

		objBytes[0] = (byte)Priority;
		objBytes[1] = PacketHeader.ObjectID;
		objBytes[2]	= PacketHeader.MessageID;
		objBytes[3] = PacketHeader.TotalSize;
		objBytes[4] = PacketHeader.PacketLength;
		objBytes[5] = PacketHeader.Offset;
		objBytes[6] = (byte)CommandType;

		for(int i = 0; i < PacketHeader.PacketLength; i++)
		{
			objBytes[i + 7] = m_Message[i];
		}

		return objBytes;
	}

	/// <summary>
	/// Converts the bytes to a packet object
	/// </summary>
	/// <param name="RawBytes"></param>
	public void LoadFromRaw(byte[] RawBytes)
	{
		

		this.Priority = (GameCommand.CommandPriority)RawBytes[0];
		this.PacketHeader.ObjectID = RawBytes[1];
		this.PacketHeader.MessageID = RawBytes[2];
		this.PacketHeader.TotalSize = RawBytes[3];
		this.PacketHeader.PacketLength = RawBytes[4];
		this.PacketHeader.Offset = RawBytes[5];
		this.CommandType = (GameCommand.CommandType)RawBytes[6];

		this.Message = System.Text.Encoding.UTF8.GetString(RawBytes, 7, MAXPACKETLENGTH - 7).Trim('\0');
	}

	/// <summary>
	/// Sealed class containging header information
	/// </summary>
	public sealed class Header
	{
		public byte ObjectID;
		public byte MessageID;
		public byte TotalSize;
		public byte PacketLength;
		public byte Offset;
	}
}

The command Queue class
/// <summary>
/// Class that contains all lists for inbound commands and outbound packets.
/// </summary>
public class CommandQueue
{
	/// <summary>
	/// Timer that will send all outbound items
	/// </summary>
	private static Timer sendTimer;

	/// <summary>
	/// List containing all outbound packets to be sent
	/// </summary>
	private static ArrayList OutboundPackets = new ArrayList();

	/// <summary>
	/// All commands that have been successfully built
	/// </summary>
	public static ArrayList IncomingCommands = new ArrayList();

	/// <summary>
	/// Fragments of commands that have not been built into commands yet
	/// </summary>
	public static ArrayList CommandFragments = new ArrayList();

	/// <summary>
	/// Starts the timer for sending the Queue
	/// </summary>
	/// <param name="SecondsBetweenSends">How many seconds to wait between sending the Queue</param>
	public static void Start(double SecondsBetweenSends)
	{
		sendTimer = new Timer();
		sendTimer.Elapsed += new ElapsedEventHandler(SendQueue);
		sendTimer.Interval = SecondsBetweenSends * 1000;
		sendTimer.Enabled  = true;
	}

	/// <summary>
	/// Parses command into packets to be added to outbound Queue
	/// </summary>
	/// <param name="objCommand"></param>
	public static void SendToOutBoundQueue(GameCommand objCommand)
	{
		//Make sure the MessageQueue is instantiated
		if(OutboundPackets == null)
			OutboundPackets = new ArrayList();

		//Make sure this command has somewhere to go
		if(objCommand.DestSock == null)
			throw new Exception("Command in Queue without Destination Socket set");

		//Get the total size of the command
		int intTotalSize = objCommand.Text.Length;

		//Set the amount of bytes left
		int intBytesLeft = intTotalSize;

		//Set the default offset
		int intOffset = 0;

		//Generate a random number for this
		int intMessageID = MiscUtils.GetRandom(0, 64);

		//This is a new packet so the packet len is 0
		int intPacketLen = 0;

		//Do this until there is nothing left
		while(intBytesLeft > 0)
		{
			intPacketLen = intBytesLeft;
				
			int intMaxPacketLen = QueuePacket.MAXPACKETDATALENGTH;
				
			//Make sure this packet isn't too big to fit
			if(intPacketLen > intMaxPacketLen)
				intPacketLen = intMaxPacketLen;

			//Create a new packet class
			QueuePacket objPacket = new QueuePacket();

			objPacket.PacketHeader.MessageID = (byte)intMessageID;
			objPacket.PacketHeader.TotalSize = (byte)intTotalSize;
			objPacket.PacketHeader.Offset = (byte)intOffset;
			objPacket.PacketHeader.PacketLength = (byte)intPacketLen;
				
			objPacket.PacketHeader.ObjectID = (byte)objCommand.ObjectID;

			//Set the dest
			objPacket.DestSock = objCommand.DestSock;

			//Set command info
			objPacket.Message = objCommand.Text.Substring(intOffset, intPacketLen);
			objPacket.CommandType = objCommand.Type;
			objPacket.Priority = objCommand.Priority;
			
			//Add this packet to the outbound queue
			OutboundPackets.Add(objPacket);

			//Remove the bytes
			intBytesLeft -= intPacketLen;

			//Increment the offset
			intOffset += intPacketLen;
				
		}
	}

	/// <summary>
	/// Adds a packet fragment to the Fragment list, and then tries to build a command out of
	/// all pieces of the same fragment
	/// </summary>
	/// <param name="objPacket">Fragment to be added to the queue</param>
	public static void AddFragment(QueuePacket objPacket)
	{
		CommandFragments.Add(objPacket);
		BuildCommandFromFragments(objPacket.PacketHeader.MessageID);
	}

	/// <summary>
	/// Sends all queued up packets to their destination
	/// </summary>
	/// <param name="sender">Not used</param>
	/// <param name="e">Not used</param>
	private static void SendQueue(object sender, ElapsedEventArgs e)
	{
		if(OutboundPackets != null)
		{
			for(int i = 0; i < OutboundPackets.Count; i++)
			{
				QueuePacket objPacket = (QueuePacket)OutboundPackets[i];
				objPacket.Send();
				//Low priority packets don't need a ack
				if(objPacket.Priority == GameCommand.CommandPriority.Low)
				OutboundPackets.RemoveAt(i);

			}
		}
	}

			
	private static void BuildCommandFromFragments(byte MessageID)
	{
			
		//Has the first piece arrived? If not, something is wacky and we need to leave
		if (!FindFirstFragment(MessageID))
		{
			return;
		}

				
		//This contains all packets with matching MessageID
		ArrayList objFilteredPackets = new ArrayList();

		//Pull all matching packets
		foreach(QueuePacket objPacket in CommandFragments)
		{
			if(objPacket.PacketHeader.MessageID == MessageID)
				objFilteredPackets.Add(objPacket);
		}

		//If there is just one in here, thats the message that triggered this
		//If there is just one packet, it shouldn't be in here anyway.
		//Bye
		if(objFilteredPackets.Count <= 1)
			return;

		//We have two or more packets now, we can check to see if all pieces are there
		int intTotalPacketLength = ((QueuePacket)objFilteredPackets[0]).PacketHeader.TotalSize;
		int intTotalPacketsNeeded = 0;

		//Determine if there are even-sized packets in this
		if((intTotalPacketLength % QueuePacket.MAXPACKETDATALENGTH) == 0)
		{
			intTotalPacketsNeeded = intTotalPacketLength / QueuePacket.MAXPACKETDATALENGTH;
		}
		else
		{
			intTotalPacketsNeeded = (intTotalPacketLength / QueuePacket.MAXPACKETDATALENGTH) + 1;
		}

		//If there arent' enough packets in there, no point in continuing
		if(objFilteredPackets.Count < intTotalPacketsNeeded)
			return;

		//TODO: VALIDATE PACKETS HERE
				
		//Put packets into a byte array
		byte[] CompleteMessage = new byte[intTotalPacketLength];
			
		foreach(QueuePacket objPacket in objFilteredPackets)
		{
			if(objPacket.PacketHeader.PacketLength != objPacket.Message.Length)
			{
				Debug.WriteLine("Packet Failed Length Assert!");
			}
			int offset = objPacket.PacketHeader.Offset;
			for(int i = 0; i < objPacket.MessageBytes.Length; i++)
			{
				try
				{
					CompleteMessage[offset + i] = objPacket.MessageBytes[i];
				}
				catch
				{
					Debug.WriteLine("Error in Building packets: " + intTotalPacketLength.ToString());
					Debug.WriteLine("Error in Building packets: " + offset.ToString() + " - " + i.ToString() + " -- "  + objPacket.Message);
				}
			}
		}

		//Whee, done building!
		//Create a game command and add it to the incoming command buffer
		GameCommand objCommand = new GameCommand();

		objCommand.Type = ((QueuePacket)objFilteredPackets[0]).CommandType;
		objCommand.Priority = ((QueuePacket)objFilteredPackets[0]).Priority;
		objCommand.Text = System.Text.Encoding.UTF8.GetString(CompleteMessage);
		objCommand.ObjectID = ((QueuePacket)objFilteredPackets[0]).PacketHeader.ObjectID;

		IncomingCommands.Add(objCommand);
			
	}

	/// <summary>
	/// Returns a true or false if the initial portion of the current message has been
	/// received and stored as a fragment
	/// </summary>
	/// <param name="MessageID">ID of the first packet to look for</param>
	/// <returns></returns>
	private static bool FindFirstFragment(byte MessageID)
	{
		foreach(QueuePacket objPacket in CommandFragments)
		{
			if(objPacket.PacketHeader.MessageID == MessageID && objPacket.PacketHeader.Offset == 0)
				return true;
		}
		return false;
	}
}

This whole process really doesn't generate any overhead for the server, so it might be feasible to scale up. However, I'm not sure, and would love to have some feedback :D Thanks in advance, and sorry about the long post. PS: Special thanks to Winter, as I mainly modified some of his C code to C#. I hate pointers :D Edit: put the code in "source" tags so the long lines don't ruin the regular text. [Edited by - hplus0603 on November 27, 2005 8:56:07 PM]

Share this post


Link to post
Share on other sites
Siberwulf    163
Bump and thanks for the help on the source tags. Should have that on the left side, like most other boards, but now I know!


Anyone have any ideas on whether this looks good or not?

Share this post


Link to post
Share on other sites
_winterdyne_    530
Don't thank me - the code I put up was refactored from Planeshift.

It seems ok to me from skim-reading it, but I would point out one thing - you're referring to your message as a 'command'. This is bad practice - your network message might not be a command, and if you get yourself into the mindset that all your netcode is for is passing that command you'll find it tricky to spot bugs. Just a psychological thing, but I do recommend you call a message a message. :-) You'll need to validate that it IS a command before you can call it that.

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