The NetworkPacket (Ep. 2)

The NetworkPacket (Ep. 2)

T

Before we get to the actual coding I’ll explain a bit about why we will need this class.

As you’ve probably guessed from the name it will deal with network packets. However, these are not the actual bit packets that are sent by the TCP layer but a self-contained data packet from the perspective of our setup. It will provide us with a standardized way of knowing what to do and get the data sent for that specific action within our client and server.
Another important characteristic of these NetworkPackets is the code and the way the data will be stored within it.

The NetworkPacket code

The code set on the NetworkPacket is how the server and the client know which action to trigger, hence, they have to be unique and stored in a dictionary accessible to both the server and the client. In order to make sure we have the same codes defined on both the client and the server, it is a good idea to also export that dictionary as a standalone DLL. However, due to the fact that during development this is likely to change a lot, for now we’ll keep it in each project as a copy and make sure to dupliate the changes.

Since we’re talking about the network packet codes this would be a good time to go over the related PacketCode dictionary class.

The PacketCode dictionary

I’ve called this class a dictionary because that is the scope it will fulfill however, it is not actually a dictionary as in the data type “dictionary”. It is a way to store some numbers associated with a key and have it be static so we can easily create NetworkPackets by passing in the key of the code we want instead of having to remember the numbers from memory or having to use cast operators to convert from an enum to a int.

A particular thing to take note within this class and in general when talking about packet codes is the type of these codes: double. I’ve made them double because sometimes you might want to send simply a code, with no data, which would make a very small NetworkPacket to be sent over the Socket. Couple this with the fact that sockets don’t necesarily send the data immediatly unless it passes a certain threshold and it should make sense why we’re using double instead of integer for the code type.

In the snippet below you’ll find the PacketCode class alongside some examples and instructions on how to define a new code.

// This class has no dependencies so we can jump straight in
public class PacketCode
{
    // Does pretty much what it says on the tin. As explained in the post above
    // this is a double to deal with the way Sockets actually send data. Other
    // than that it's simply a number, any number you want for a specific packet
    readonly double Code;

    // Simple contructor, takes a double and sets it as the internal code
    public PacketCode(double code)
    {
        Code = code;
    }

    // Here we define an operator that will conver a PacketCode to a double
    public static implicit operator double(PacketCode packetCode)
    {
        return packetCode.Code;
    }

    // Since we can convert a PacketCode to a double we'll now define a way to
    // convert a double to a packet code
    public static implicit operator PacketCode(double code)
    {
        return new PacketCode(code);
    }


    // This is all the core functionality of teh class, however we still need to
    // define the "dictionary" part of it.
    // In order to define a new packet code follow the format below:
    // public static readonly PacketCode <Packet_code_name> = <numeric_value>;
    // Below, you can see a few packet codes defined and set to specific values
    
    /*
     * Server Packets (1000 to 2000)
     * Sent by the server
     */

    // Connection packets (1000 to 1100)
    public static readonly PacketCode S_Connection_OK = 1000;
    public static readonly PacketCode S_Connection_ACK = 1001;
    public static readonly PacketCode S_Connection_CLOSE = 1002;
    public static readonly PacketCode S_Connection_DROP = 1003;

    // Server Utils packets (1100 to 1200)
    public static readonly PacketCode S_Message = 1100;
    public static readonly PacketCode S_HeartBeat = 1101;

    // Server Player packets (1200 to 1300)
    public static readonly PacketCode S_Existing_PlayerData = 1200;
    public static readonly PacketCode S_Current_PlayerData = 1201;
    public static readonly PacketCode S_Player_Joined = 1202;
    public static readonly PacketCode S_Player_Left = 1203;

    /*
     * client packets (2000 to 3000)
     * sent by the client
     */

    // Connection packets (2000 to 2100)
    public static readonly PacketCode C_Connection_OK = 2000;
    public static readonly PacketCode C_Connection_ACK = 2001;
    public static readonly PacketCode C_Connection_CLOSE = 2002;
    public static readonly PacketCode C_Connection_DROP = 2003;

    // Client Utils packets (2100 to 2200)
    public static readonly PacketCode C_HeartBeat = 2100;

    // Client Player packets (2200 to 2300)
    public static readonly PacketCode C_Player_Ready = 2200;
    public static readonly PacketCode C_Player_Login = 2210;
    public static readonly PacketCode C_Player_Register = 2211;
    
    // You can use a different naming convention and/or change the values of
    // each of the packets above. You could potentially also use fractional
    // values but I think that differentiating between 2100 and 1100 is easier
    // than trying to distinguish between 21.00 and 11.00
}

If you don’t understand how each code will be used don’t worry. As we progress through this series things should become clear and it will become apparent how we use each code.

The NetworkPacket data

The data will be stored as a serialised JSON representing the data structures required for each action. There are better ways to deal with this, for example it would make a lot more sense to serialize to a byte array dirrectly instead of a string and then a byte array but for now JSONs will be a lot easier to read whilst debugging or logging than byte arrays would be.

As with the previous class, this is not how you would leave things in production so make sure to keep following this series so that by the end as much of it as possible has been updated to the optimised form.

The NetworkPacket class code

// This class depends on the ByteBuffer class (defined in the previous post of this series)
// and the PacketCode class (defined earlier within this post). Since both of those classes
// are part of the same (global) namespace as this one we don't need to import them.
public class NetworkPacket
{
    // As mentioned, each NetworkPacket has to have a code, more specifically a value from
    // those defined within the PacketCode dictionary.
    private readonly PacketCode Code;
    
    // Besides the code, each NetworkPacket might have some data associated with it. Both
    // the code and the data properties are set as readonly because once the packet has
    // been created these should not change.
    private readonly string Data;

    // A simple straight forward constructor, take a code and some data and creates a new
    // NetworkPacket.
    public NetworkPacket(PacketCode code, string data = "")
    {
        Code = code;
        Data = data;
    }

    // Since the properties are marked as private we need to create getters, below is the
    // one for the packet's code followed by the one for the data.
    public PacketCode GetCode()
    {
        return Code;
    }

    public string GetData()
    {
        return Data;
    }
    
    // We will begin by defining an implicit conversion operator from a NetworkPacket to
    // a ByteBuffer. This will become usefull a little bit later.
    public static implicit operator ByteBuffer(NetworkPacket packet)
    {
        // We create a new ByteBuffer instance
        ByteBuffer byteBuffer = new ByteBuffer();
        
        // I have chosen this convention where I first write the packet's code followed by
        // the packet's data. You can choose a different one as long as you keep in mind
        // that, as stated in the previous post, you'll have to read the data in the same
        // order that you've written it to the buffer.
        // First we write the packet's code (which is implicitly converted from a 
        // PacketCode to a double) to the buffer.
        byteBuffer.WriteDouble(packet.GetCode());
        
        // Then we write the packet's data, which is a string, to the buffer.
        byteBuffer.WriteString(packet.GetData());
        
        // And finally, we return the newly created ByteBuffer instance.
        return byteBuffer;
    }

    // Now we'll write a few lines of code defining the reverse of the conversion defined
    // above.
    public static implicit operator NetworkPacket(ByteBuffer byteBuffer)
    {
        // We start by reading the packet's data, first the code, then the data, same 
        // order we wrote them in.
        double packetCode = byteBuffer.ReadDouble();
        string packetData = byteBuffer.ReadString();
        
        // Since we've read all the data we need from the buffer we can dispose of it,
        // freeing some memory
        byteBuffer.Dispose();
        
        // With the read code and data we create a new NetworkPacket and return it
        return new NetworkPacket(packetCode, packetData);
    }
    
    // As we need to send a byte array over the Socket we will define an implicit converison
    // operator from a NetworkPacket to a byte array. This is where the previously defined
    // conversion operators will come in handy.
    public static implicit operator byte[](NetworkPacket packet)
    {
        // Since we can now implicitly convert a NetworkPacket to a ByteBuffer we'll begin
        // by doing just that. This will create a ByteBuffer instance containing all the
        // data of the packet.
        ByteBuffer byteBuffer = packet;
        
        // Since we also defined an implicit conversion from a ByteBuffer to a byte array
        // we can use that conversion and simply return the newly created ByteBuffer. This
        // will in effect convert the ByteBuffer to a byte array and return that.
        return bytesBuffer;
    }

    // Finally, we will define a conversion operator from a byte array to a NetworkPacket.
    // Same as with the previous one, we'll make use of previously defined conversion
    // operators and methods.
    public static implicit operator NetworkPacket(byte[] bytes)
    {
        // Since we can convert a byte array to a ByteBuffer we'll begin by doing that. This
        // will in effect create a new ByteBuffer instance containing all the data for a
        // NetworkPacket.
        ByteBuffer byteBuffer = bytes;
        
        // Given that we already defined a conversion from a ByteBuffer to a NetworkPacket we
        // can now use that in order to create a new NetworkPacket instance from the given
        // ByteBuffer one.
        NetworkPacket packet = byteBuffer;
        
        // Finally, we return the previously created NetworkPacket instance.
        return new packet;
    }
}

Closing notes

With these two classes wrapped up next time we’ll be ready to actually start work on the server itself. If you see any modifications you feel comfortable doing in order to optimise these provided classes, feel free to do so. As long as you maintain the same signatures for the classes and methods you should be able to follow along regardless.