Developing Client/Server Architecture


Hello, 

I'm the solo developer working on Antiutopia. I've built this game completely from scratch using C++, OpenGL, and Blender. Today's post focuses on the networking serialization system I implemented.

I'm a senior at a small college in southern ohio, Shawnee State University, and during one of my courses we focused heavily on TCP/UDP-networking protocols. I decided to expand out one of our small UDP-based labs and built a simple binary serialization for communicating back and forth with the Client/Server. This essentially became the groundwork for the entire project-- Antiutopia.

When the Client and Server communicate back and forth; position, rotation, input state, etc.  They essentially need to send raw byte data, instead of specific C++ types. TCP/UDP packets are nothing more than just byte streams.  A major issue arises with sending data this way-- is that different CPUs use different endianness when storing integers in memory. 

If you think about what a uint32_t actually is, four bytes of memory:

[value] = [byte0][byte1][byte2][byte3]

But in RAM, integers are not stored as four separate bytes in a guaranteed format. CPU architectures differ in how they order the bytes, and you can’t rely on the system’s native layout.  For example:

value = 0x11223344

Some machines (little-endian) will store this as:

 0x44332211

While others (big-endian) will store it as:

 0x11223344

Sending a uint32_t directly from memory, I'd have no control over how the receiving machine interprets its bytes. So I have to manually extract that information in a fixed byte order.  This is where my binary serialization comes into play-- forcing a consistant layout. 

I wrote a Serializer namespace containing serveral helper functions, but they basically all derive from two core functions:

  1. writeUInt32(std::vector<uint8_t>& buffer, uint32_t value)
    Takes a 32-bit integer (uint32_t) and writes it into a byte buffer in big-endian order.

  2. readUInt32(const std::vector<uint8_t>& buffer, size_t& offset)
    Reads 4 bytes from a buffer (also in big-endian order) starting at a given offset, reconstructs the original uint32_t, and moves the offset forward.

These serialization functions give me precise control over the structure of every packet. Inside writeUint32(), I perform several bit-shift operations to move each byte into the correct position and push it into the buffer. readUint32() simply reverses this process, and reconstructs the original integer by shifting each byte back into place. This allows me to send packets of data back and forth, controlling the exact binary format. 

Thank you for taking the time to read my article, and if you have any questions please feel free to ask!

Nicholas Ratliff

Leave a comment

Log in with itch.io to leave a comment.