AlgoDaily

Lesson 3: Game Client - Networking

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Client Goes Online

Why networking matters now

In Lesson 2: Game Client - Basics, we separated the client into subsystems, treated input as intent, and introduced client states. That gives us a clean place to plug in networking without turning rendering, input, and world simulation into one tangled loop.

Today we’ll add the client-side networking layer for our MMORPG prototype:

  • connect/disconnect lifecycle
  • packet/message boundaries
  • outgoing player intents
  • incoming world updates
  • a thread-safe handoff between networking and the main loop

We will not build the real authoritative server yet — that begins in Lesson 4: Game Server - Basics. For now, we’re shaping the client so it can talk to one cleanly.

Client Goes Online

Networking Boundary

Keep networking behind a boundary

Coming from Netty/Spring, you already know the value of isolating protocol code from application logic. We want the same discipline in C++.

A useful first client shape is:

Input System       World View / Renderer
     |                    ^
     v                    |
Intent Queue ---> Game Loop State <--- Incoming Message Queue
                          ^
                          |
                  Network Client

The main loop should not directly call low-level socket reads everywhere. Instead:

  • input creates intent messages: move, interact, cast spell
  • network receives server messages: snapshot, entity spawned, entity removed
  • the main loop drains incoming messages at a safe point each frame
  • rendering reads the client’s local view of the world

This preserves the subsystem boundaries from Lesson 2 while making the client online-capable.

Networking Boundary

Client Authority Check

Quick check

Which message should the client generally send to the server in an authoritative MMORPG design?

Connection Lifecycle

Treat connection as client state

Networking is not just connect() and then happy gameplay forever. The client needs a small lifecycle that integrates with the client states from Lesson 2.

A minimal lifecycle:

Disconnected
    |
    v
Connecting
    |
    v
Connected
    |
    v
Authenticating
    |
    v
InWorld

And failure can happen almost anywhere:

Connecting ----fail----> Disconnected
Connected -----drop----> Disconnected
InWorld -------timeout-> Disconnected

For this lesson, focus on client behavior:

  • show connection progress
  • prevent gameplay input before InWorld
  • cleanly clear network-owned state on disconnect
  • keep retry/backoff logic separate from rendering

Authentication details and account persistence belong later; here we only need the state slots that will support them.

Connection Lifecycle

Packet Framing

TCP is a byte stream, not a message stream

If we start with TCP, we get reliable ordered bytes. But we do not get automatic game-message boundaries.

That means one socket read might contain:

  • half a message
  • exactly one message
  • multiple messages
  • the end of one message plus the start of another

So the client protocol needs framing. A simple first format is:

[4-byte length][payload bytes]
[4-byte length][payload bytes]

Below is a standalone parser demo. It simulates receiving chunks from the network and reconstructs complete messages.

```cpp
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

class FrameParser {
public:
    void feed(const std::vector<uint8_t>& bytes) {
        buffer.insert(buffer.end(), bytes.begin(), bytes.end());
        parseAvailable();
    }

private:
    std::vector<uint8_t> buffer;

    static uint32_t readU32BE(const std::vector<uint8_t>& b, size_t offset) {
        return (static_cast<uint32_t>(b[offset]) << 24) |
               (static_cast<uint32_t>(b[offset + 1]) << 16) |
               (static_cast<uint32_t>(b[offset + 2]) << 8) |
               static_cast<uint32_t>(b[offset + 3]);
    }

    void parseAvailable() {
        while (true) {
            if (buffer.size() < 4) return;

            uint32_t length = readU32BE(buffer, 0);
            if (buffer.size() < 4 + length) return;

            std::string payload(buffer.begin() + 4, buffer.begin() + 4 + length);
            std::cout << "Parsed message: " << payload << "\n";

            buffer.erase(buffer.begin(), buffer.begin() + 4 + length);
        }
    }
};

std::vector<uint8_t> frame(const std::string& payload) {
    uint32_t len = static_cast<uint32_t>(payload.size());
    std::vector<uint8_t> out;
    out.push_back((len >> 24) & 0xFF);
    out.push_back((len >> 16) & 0xFF);
    out.push_back((len >> 8) & 0xFF);
    out.push_back(len & 0xFF);
    out.insert(out.end(), payload.begin(), payload.end());
    return out;
}

int main() {
    std::vector<uint8_t> stream;
    std::vector<uint8_t> a = frame("WELCOME");
    std::vector<uint8_t> b = frame("SNAPSHOT x=10 y=20");
    stream.insert(stream.end(), a.begin(), a.end());
    stream.insert(stream.end(), b.begin(), b.end());

    FrameParser parser;

    parser.feed(std::vector<uint8_t>(stream.begin(), stream.begin() + 3));
    std::cout << "Fed first 3 bytes: no full message yet\n";

    parser.feed(std::vector<uint8_t>(stream.begin() + 3, stream.begin() + 12));
    parser.feed(std::vector<uint8_t>(stream.begin() + 12, stream.end()));

    return 0;
}
```

TCP Message Myth

True or false?

With TCP, if the server sends one complete game message, the client’s next socket read is guaranteed to return exactly that one message.

Tick Handoff

Network updates meet the game loop

Your client loop from Lesson 2 remains the coordinator. Networking should feed it, not control it.

A practical frame looks like:

while running:
    poll window/input
    convert input -> intents
    enqueue outgoing intents
    drain incoming network messages
    apply messages to client world view
    update local interpolation/prediction
    render

Two important rules:

  1. Do not mutate render/world state from a socket callback directly. Queue messages and apply them on the game thread.
  2. Do not block the frame waiting for the network. A stalled connection should not freeze the window.

This is similar to event-loop thinking from web and Netty systems, but in games the render frame budget makes blocking especially painful.

Tick Handoff

Name The Boundary

Fill in the blank

Because TCP is a byte stream, the protocol layer must add message boundaries. This technique is called ________.

Queues Between Threads

A safe client-side handoff

Many C++ clients use one thread for the game loop and another for blocking network I/O. Even if you later switch to non-blocking sockets or an async library, the same idea holds: network code produces messages; the game loop consumes them at a controlled point.

This standalone example simulates a network thread receiving messages and the main thread draining them. It uses standard C++ only, so it runs without platform socket setup.

```cpp
#include <chrono>
#include <iostream>
#include <mutex>
#include <queue>
#include <string>
#include <thread>

class MessageQueue {
public:
    void push(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex);
        messages.push(msg);
    }

    bool tryPop(std::string& out) {
        std::lock_guard<std::mutex> lock(mutex);
        if (messages.empty()) return false;
        out = messages.front();
        messages.pop();
        return true;
    }

private:
    std::mutex mutex;
    std::queue<std::string> messages;
};

int main() {
    MessageQueue incoming;
    bool networkDone = false;

    std::thread networkThread([&]() {
        const char* simulatedMessages[] = {
            "WELCOME playerId=42",
            "ENTITY_SPAWN id=100 type=slime",
            "SNAPSHOT player=42 x=11 y=20"
        };

        for (const char* msg : simulatedMessages) {
            std::this_thread::sleep_for(std::chrono::milliseconds(120));
            std::cout << "[network] received: " << msg << "\n";
            incoming.push(msg);
        }

        networkDone = true;
    });

    int frame = 0;
    while (!networkDone || frame < 8) {
        std::cout << "[game] frame " << frame << "\n";

        std::string msg;
        while (incoming.tryPop(msg)) {
            std::cout << "[game] applying: " << msg << "\n";
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(60));
        ++frame;
    }

    networkThread.join();
    std::cout << "Client shutdown cleanly\n";
    return 0;
}
```

Ready For Server

What we have now

Our client now has a networking shape that fits the architecture from the first two lessons:

  • a clear network subsystem boundary
  • a connection lifecycle that maps into client states
  • length-prefixed framing for byte streams
  • outgoing intent messages instead of authoritative claims
  • incoming message queues applied safely during the game loop

Before moving on, your client should be able to log a simulated connection flow like:

Disconnected -> Connecting -> Connected -> InWorld
send: MoveIntent(dx=1, dy=0)
recv: Snapshot(entity=42, x=10, y=20)
apply snapshot during frame

In Lesson 4: Game Server - Basics, we’ll build the other side of this conversation and make these messages come from an actual server process.

Ready For Server

Open interactive view →