The ByteBuffer (Ep. 1)

The ByteBuffer (Ep. 1)

T

This class will be used to read an array of bytes from the socket and then provide use easy methods for extracting the information from said array into more easily workable data types such as int, float, string, etc. Besides reading this class will also allow us to write data to an array of bytes. This is needed because the socket can only send/receive arrays of bytes so sending an int without conversion is impossible (as far as I know).

It has only three dependencies:  “System.Collections.Generic“, “System.Text” and “System“. Bellow you can find the entire class with comments at every single step to explain why that bit is needed and how it works.

// Include required dependencies
using System.Collections.Generic;
using System.Text;
using System;

// Define class to implement the IDisposable interface. 
// This will help provide an easy way to clear the memory used by this object.
public class ByteBuffer : IDisposable
{
    // First of all we need to store a list of bytes. As we receive data from sockets this is where 
    // that data will be stored. I used a list because it has a few helpers that will come in handy 
    // a bit later.
    private List<byte> buffer;
    
    // This is set to be a clone of the previous property whenever we read data from the class. 
    // I've chosen this setup due to easy position manipulation. More on this below.
    private byte[] readBuffer;
    
    // The readPosition property we'll use to keep a cursor within the array and know what 
    // position we're reading from the buffer.
    private int readPosition;

    // Finally we need to store a flag that tells us whether the buffer has been updated since the 
    // last read or not
    private bool bufferUpdated = false;

    // Now it's time to set the constructor of this class.
    public ByteBuffer()
    {
    // Initialize the list that stores the data
        buffer = new List<byte>();
        
    // Set the byte array clone to null for now
        readBuffer = null;
        
    // Set the read position to 0
        readPosition = 0;
    }

    // A simple method, this one simply calls the ToArray() mehtod on the data buffer and returns 
    // the result.
    public byte[] ToArray()
    {
        return buffer.ToArray();
    }
    
    // Another simple method that will return the current position of the cursor.
    public int GetReadPosition()
    {
        return readPosition;
    }

    // So far it's all pretty basic, this simple method purely returns the number of bytes in the 
    // list that we use to store the data in.
    public int Length()
    {
        return buffer.Count;
    }

    // Not only do we want to know where we are currently in the byte buffer, we also need to 
    // know how many more bytes there are left within the buffer. The method is simply subtracting 
    // the current read position from the entire length of the byte buffer.
    public int RemainingLength()
    {
        return Length() - GetReadPosition();
    }

    // Still in the area of simple methods, we can use this method to clear the buffer of all of its 
    // data.
    public void Clear()
    {
        buffer.Clear();
        readBuffer = null;
        bufferUpdated = true;
        readPosition = 0;
    }

    // Here it gets a bit more interesting. This method takes a byte array (presumably the data 
    // received from the socket) and adds it to the byte list called buffer.
    public void WriteByteArray(byte[] input)
    {
        // The AddRange method can be used to add an array of elements to a list containing elements 
        // of the same type.
        buffer.AddRange(input);
        
        // Mark the buffer as updated so that on read we can update a few things.
        bufferUpdated = true;
    }

    // Sometimes we don't need to write an entire array of bytes but merely a single one. This method 
    // should be simple enough to not require any complex explanation about what's going on.
    public void WriteByte(byte input)
    {
        buffer.Add(input);
        bufferUpdated = true;
    }

    // Now that we can write a byte and an array of bytes to the buffer we can use those as helper 
    // methods to write more complex data types to the buffer.
    public void WriteInt(int input)
    {
        // Here we take an integer <input> and convert that into a byte array using the BitConverter 
        // class (part of the System) and then writing said int to the buffer using the previously 
        // defined WriteByteArray method
        WriteByteArray(BitConverter.GetBytes(input));
        
        // Since we're calling the helper method defined beforehand we don't need to set the 
        // bufferUpdated flag to true again in here.
    }

    // Since we want to be able to send more than just integers and byte arrays we will define a 
    // few more methods dealing with taking an input of a certain type, converting that into a byte 
    // array and then writing said byte array to the buffer.
    // This method deals with taking a float and putting it through the same process as above.
    public void WriteFloat(float input)
    {
        WriteByteArray(BitConverter.GetBytes(input));
    }

    // This method deals with taking a double and putting it through the same process as above.
    public void WriteDouble(double input)
    {
        WriteByteArray(BitConverter.GetBytes(input));
    }

    // This method deals with taking a string and putting it through the same process as above. 
    // However, strings require a special format when stored into a byte buffer in order for us to 
    // be able to properly retrieve the entire string but only this string. Before converting the 
    // actual string into a byte array we find its length and put that in the buffer (converted to 
    // a byte array), followed by the string converted to a byte array.
    public void WriteString(string input)
    {
        // Find the string's length, convert it to a byte array and write it to the buffer.
        WriteByteArray(BitConverter.GetBytes(input.Length));
        
        // Take the entire actual string, convert it to a byte array and append it to the buffer.
        WriteByteArray(Encoding.ASCII.GetBytes(input));
    }

    // Now that we've defined a few helper methods covering all the expected data types we'll work 
    // with we can move on to defining some helper methods for getting the data out.
    // This method will read an array of bytes from the buffer and return it as is. To be able to 
    // do that we need to know how many elements to return out of the buffer (length parameter). 
    // On top of that, sometimes we need to get some data without moving the read cursor, if seek 
    // was false that's what would happen.
    public byte[] ReadByteArray(int length, bool seek = true)
    {
        // First we need to check that we have enough elements left in the buffer.
        if (buffer.Count > readPosition + length - 1)
        {
            // If we do we need to figure out if the buffer has been updated since the last read.
            if (bufferUpdated)
            {
                // If it has, we clone the byte list to a byte array
                readBuffer = buffer.ToArray();
                
                // Also, we set the flag to false since we're currently doing a read.
                bufferUpdated = false;
            }
    
            // Here we'll be extracting a subset of the list elements and add them to a byte array,
            // starting at readPosition and going for length number of elements.
            byte[] value = buffer.GetRange(readPosition, length).ToArray();
            
            // If the seek flag is true, we move the cursor to after the last element we've read. 
            // Otherwise we leave the cursor where it is causing the next read to read the same 
            // data as the previous one.
            if (seek & buffer.Count > readPosition)
            {
                readPosition += length;
            }
    
            // Return the selected byte array
            return value;
        }
        else
        {
            // This could be improved by throwing a more specific expection type but for the scope
            // of this tutorial this will do.
            throw new Exception("Buffer is past its limit!");
        }
    }

    // Again, on top of fetching an array of bytes sometimes we need to fetch just the one. The 
    // process is the same as before, in fact we could have just called the previous method with a 
    // length of 1.
    public byte ReadByte(bool seek = true)
    {
        if (buffer.Count > readPosition)
        {
            if (bufferUpdated)
            {
                readBuffer = buffer.ToArray();
                bufferUpdated = false;
            }

            byte value = readBuffer[readPosition];
            if (seek & buffer.Count > readPosition)
            {
                readPosition += 1;
            }

            return value;
        }
        else
        {
            throw new Exception("Buffer is past its limit!");
        }
    }

    // Moving forward an important thing to know is how many bytes does each data type require.
    // This method will attempt to read an int from the buffer. As with the write portion of the 
    // class we'll make use of the previously defined methods as helpers to make our life easier.
    public int ReadInt(bool seek = true)
    {
        // Since an integer requires 4 bytes we read an array of 4 bytes from the buffer
        byte[] readBytes = ReadByteArray(4, seek);
        
        // Convert the array of 4 bytes we've read to an integer value starting from position 0
        return BitConverter.ToInt32(readBytes, 0);
    }
    
    // Using the same process as we did for integers we'll attempt to read a float from the 
    // buffer.
    public float ReadFloat(bool seek = true)
    {
        // Since a float requires 4 bytes we read an array of 4 bytes from the buffer
        byte[] readBytes = ReadByteArray(4, seek);
        
        // Convert the array of 4 bytes we've read to a float value starting from position 0
        return BitConverter.ToSingle(readBytes, 0);
    }

    // Since we can write doubles it would be good to be able to read them too, no? We use the 
    // same process as above to do so.
    public double ReadDouble(bool seek = true)
    {
        // Since a double requires 8 bytes we read an array of 8 bytes from the buffer
        byte[] readBytes = ReadByteArray(8, seek);
        
        // Convert the array of 8 bytes we've read to a double value starting from position 0
        return BitConverter.ToDouble(readBytes, 0);
    }

    // And finally, we need to write a method that will allow us to read strings from the buffer.
    public string ReadString(bool seek = true)
    {
        // Remember when we were writing the string to the buffer how we first of all wrote the 
        // length of the string beforehand? Since this is the reverse operation we need to start 
        // by reading an integer representing the string's length.
        // Note that using an int to denote the string's length limits you to reading a text that
        // contains a maximum of 2,147,483,647 characters. So if you for some reason need to deal
        // with texts longer than that (really?!) you would need to first break the string into 
        // smaller pieces.
        int length = ReadInt(seek);

        // There's a trick here. Because the seek parameter might be false, if that's the case, 
        // we still need to move the readPosition so that the next method reads the string 
        // beginning with the first character and not the bytes representing the string's length.
        int stringReadPosition = readPosition;
        if (seek == false)
        {
            // If the seek parameter is false we move the position for this string read ahead 
            // by 4 bytes which represent the string's length and are not part of its content
            stringReadPosition += 4;
        }

        // Since we now know the length of the string, we can pass that along with the previously
        // created read position and the byte array storing a clone of the data to the method 
        // GetString of the Encoding.ASCII class in order to retrieve a string that contains 
        // length characters.
        string value = Encoding.ASCII.GetString(readBuffer, stringReadPosition, length);
        
        // As with every other method, if the seek parameter is true, move the read position. 
        // Since we did that trick before to deal with the case in which seek is false we don't 
        // need to do anything more in here.
        if (seek & buffer.Count > readPosition)
        {
            readPosition += length;
        }

        // Finally, return the string we've just read.
        return value;
    }

    // We define this here because it doesn't have much to do with the actual functionality of 
    // the class but rather with flagging whether the object is available or not.
    private bool disposedValue = false;
    
    // This method will clear the resources used by this object and mark it as disposed so that
    // we get an error if we try to use it after it's been disposed.
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                Clear();
            }
        }

        disposedValue = true;
    }

    // Disposes of the object
    // We don't need to implement the error handling about trying to use this object after it's
    // been disposed of since that's what we used the interface for.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    // And now, since we're done with all the core functionality let's add a bell and whistle. 
    // A simple implict operator defining a conversion from a byte array to a ByteBuffer and 
    // the other way around.
    public static implicit operator byte[](ByteBuffer buffer)
    {
        // This one is easy, return the ToArray() method call on the byte list
        return buffer.ToArray();
    }

    // And for creating a ByteBuffer from a byte array
    public static implicit operator ByteBuffer(byte[] byteArray)
    {
        // Create a new ByteBuffer instance
        ByteBuffer buffer = new ByteBuffer();
        
        // Write the byte array to the newly created instance
        buffer.WriteByteArray(byteArray);

        // return new instance
        return buffer;
    }
}

Before wrapping up there’s a couple of things to keep in mind whenever using this class.
First, whenever you write to the buffer you should probably keep track of the order in which things are written so that you can read in the exact same order. By example, if you write 2 integers, a float and a string, you should begin reading by first reading the 2 integers, then the float and finally the string. Otherwise “funky” stuff can happen and you’ll have to use the debugger only to realise you’ve inversed the order at some point.
Second, as you probably guessed there’s nothig to say you can’t write a ByteBuffer to a ByteBuffer. Since all it really is is a list of bytes, of course one list of bytes can contain another list of bytes. However, if you end up doing this, you should probably take a look at why you’re doing it since it doesn’t make much sense as all you would have achieved is concatenating the byte lists. Or maybe that was the point?

Finally, please don’t just copy-paste this class. I mean, I’m not going to sue you for rights but it wouldn’t teach you anything if you just grab it and run without bothering to read the comments inside it.