# Network Protocols

### Network Protocols

A network protocol is a rulebook for how programs talk over a network.

When two programs communicate, they cannot just send random bytes and hope the other side understands. They need an agreed format.

A protocol defines things like:

What messages exist?

What does each message mean?

How are messages encoded as bytes?

Who sends first?

How does the other side respond?

How are errors represented?

When does the connection close?

That agreement is the protocol.

#### A Simple Example

Imagine a tiny protocol for asking a server to echo text.

The client sends:

```text
ECHO hello
```

The server replies:

```text
OK hello
```

If the command is unknown, the server replies:

```text
ERR unknown command
```

This is a text protocol. The messages are readable.

A binary protocol might encode the same idea with bytes:

```text
01 00 00 00 05 68 65 6C 6C 6F
```

Those bytes could mean:

```text
command = 1
length = 5
payload = "hello"
```

Both styles are valid. The important part is that both programs agree.

#### Layers

Network communication is usually organized in layers.

A simplified view:

| Layer | Example | What It Handles |
|---|---|---|
| Application | HTTP, DNS, Redis protocol, PostgreSQL protocol | Meaning of messages |
| Transport | TCP, UDP | Moving bytes or packets between programs |
| Internet | IP | Moving packets between machines |
| Link | Ethernet, Wi-Fi | Moving frames on a local network |

As an application programmer, you usually work at the application layer.

For example, when writing an HTTP server, you usually do not build Ethernet frames. You read and write bytes over a TCP connection. Then you parse those bytes according to HTTP rules.

#### TCP and UDP

Most beginner network programs start with TCP.

TCP gives you a reliable byte stream. If bytes arrive, they arrive in order. TCP handles retransmission, ordering, and flow control.

But TCP does not preserve message boundaries.

If the client sends this:

```text
hello
world
```

The server might receive it as:

```text
helloworld
```

or:

```text
hel
lowor
ld
```

TCP gives you bytes, not messages.

Your protocol must define where one message ends and the next begins.

UDP is different. UDP sends datagrams. Each send corresponds to one packet-like message, but delivery is not guaranteed. Packets may be lost, duplicated, or arrive out of order.

Use TCP first. Learn UDP when you need packet-style communication, low latency tradeoffs, or custom reliability.

#### Message Framing

Message framing means deciding how to separate messages in a byte stream.

For TCP protocols, this is essential.

Common framing methods:

| Method | Example | Notes |
|---|---|---|
| newline-delimited | `PING\n` | Simple for text protocols |
| fixed-size messages | exactly 32 bytes | Easy but inflexible |
| length-prefixed | `length + payload` | Common for binary protocols |
| delimiter-based | message ends with `\0` | Simple, but escaping may be needed |
| structured format | HTTP headers plus body length | More complex but flexible |

A beginner-friendly protocol can use newline-delimited messages.

Example:

```text
PING\n
ECHO hello\n
QUIT\n
```

The server reads until newline, processes the command, then reads the next line.

#### A Tiny Text Protocol

Let’s define a small protocol.

Client commands:

```text
PING
ECHO <text>
QUIT
```

Server replies:

```text
PONG
OK <text>
BYE
ERR <message>
```

Rules:

`PING` returns `PONG`.

`ECHO hello` returns `OK hello`.

`QUIT` returns `BYE` and closes the connection.

Unknown commands return `ERR unknown command`.

Each message ends with a newline byte, `\n`.

This is enough to build a real toy protocol.

#### Parsing Commands

A parser turns bytes into meaning.

For the text protocol, the input is one line:

```zig
const line = "ECHO hello";
```

We can parse it by checking prefixes.

```zig
const std = @import("std");

const Command = union(enum) {
    ping,
    echo: []const u8,
    quit,
    unknown,
};

fn parseCommand(line: []const u8) Command {
    if (std.mem.eql(u8, line, "PING")) {
        return .ping;
    }

    if (std.mem.eql(u8, line, "QUIT")) {
        return .quit;
    }

    if (std.mem.startsWith(u8, line, "ECHO ")) {
        return .{ .echo = line[5..] };
    }

    return .unknown;
}
```

This parser does not allocate memory. It returns slices into the original line.

That is common in Zig. Parse by borrowing input when possible.

#### Handling Commands

Now write the command behavior.

```zig
fn handleCommand(writer: anytype, command: Command) !bool {
    switch (command) {
        .ping => {
            try writer.writeAll("PONG\n");
            return true;
        },
        .echo => |text| {
            try writer.print("OK {s}\n", .{text});
            return true;
        },
        .quit => {
            try writer.writeAll("BYE\n");
            return false;
        },
        .unknown => {
            try writer.writeAll("ERR unknown command\n");
            return true;
        },
    }
}
```

The return value tells the server whether to keep the connection open.

`true` means continue.

`false` means close.

#### A Simple TCP Server

Here is a small TCP server using Zig’s standard library networking APIs.

```zig
const std = @import("std");

const Command = union(enum) {
    ping,
    echo: []const u8,
    quit,
    unknown,
};

pub fn main() !void {
    const address = try std.net.Address.parseIp("127.0.0.1", 9000);

    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("listening on 127.0.0.1:9000\n", .{});

    while (true) {
        const connection = try server.accept();
        defer connection.stream.close();

        try handleConnection(connection.stream);
    }
}

fn handleConnection(stream: std.net.Stream) !void {
    var buffer: [1024]u8 = undefined;

    const reader = stream.reader();
    const writer = stream.writer();

    try writer.writeAll("READY\n");

    while (true) {
        const line_or_null = try reader.readUntilDelimiterOrEof(&buffer, '\n');

        const line = line_or_null orelse break;
        const clean_line = std.mem.trimRight(u8, line, "\r");

        const command = parseCommand(clean_line);
        const keep_going = try handleCommand(writer, command);

        if (!keep_going) {
            break;
        }
    }
}

fn parseCommand(line: []const u8) Command {
    if (std.mem.eql(u8, line, "PING")) {
        return .ping;
    }

    if (std.mem.eql(u8, line, "QUIT")) {
        return .quit;
    }

    if (std.mem.startsWith(u8, line, "ECHO ")) {
        return .{ .echo = line[5..] };
    }

    return .unknown;
}

fn handleCommand(writer: anytype, command: Command) !bool {
    switch (command) {
        .ping => {
            try writer.writeAll("PONG\n");
            return true;
        },
        .echo => |text| {
            try writer.print("OK {s}\n", .{text});
            return true;
        },
        .quit => {
            try writer.writeAll("BYE\n");
            return false;
        },
        .unknown => {
            try writer.writeAll("ERR unknown command\n");
            return true;
        },
    }
}
```

This server handles one connection at a time. That is fine for learning. Later, you can add threads, event loops, or async designs.

#### Testing with Netcat

You can test the server with `nc`:

```bash
nc 127.0.0.1 9000
```

Then type:

```text
PING
```

Expected reply:

```text
READY
PONG
```

Try:

```text
ECHO hello zig
```

Expected reply:

```text
OK hello zig
```

Try:

```text
QUIT
```

Expected reply:

```text
BYE
```

#### Why Newlines Matter

The server reads with:

```zig
reader.readUntilDelimiterOrEof(&buffer, '\n')
```

This means one message ends at newline.

Without framing, the server would not know when to process a command.

For example, if the client sends:

```text
ECHO hello
```

but never sends `\n`, the server keeps waiting. The message is incomplete according to the protocol.

Protocols need these rules. Otherwise both sides may wait forever.

#### Buffer Limits

Our example uses:

```zig
var buffer: [1024]u8 = undefined;
```

That means one command line can be at most 1024 bytes in this simple server.

This is good. Protocols should have limits.

Without limits, a client can send an extremely large line and force your server to allocate memory or wait forever.

Real protocols define maximum sizes:

maximum line length

maximum header size

maximum body size

maximum number of fields

maximum message depth

Limits are part of safe protocol design.

#### Binary Protocols

A binary protocol is usually more compact and faster to parse, but less convenient to debug manually.

A simple length-prefixed message could look like this:

```text
bytes 0..4      length, u32 little-endian
bytes 4..N      payload
```

Parser shape:

```zig
fn parseLengthPrefix(bytes: []const u8) ![]const u8 {
    if (bytes.len < 4) {
        return error.Truncated;
    }

    const len = std.mem.readInt(u32, bytes[0..4], .little);
    const start: usize = 4;
    const end = start + @as(usize, len);

    if (end > bytes.len) {
        return error.Truncated;
    }

    return bytes[start..end];
}
```

This is only the parsing idea. A real TCP reader needs to keep reading until enough bytes are available.

#### Partial Reads

Network reads are often partial.

If a protocol says the next message has 100 bytes, one read may return only 20 bytes. Your code must continue reading until it has all 100 bytes.

This is one of the most important network programming rules.

Never assume one `read` equals one message.

For TCP:

```text
send does not equal receive
write does not equal read
packet does not equal protocol message
```

Your framing code must handle this.

#### Protocol State

Some protocols have state.

Example:

```text
client connects
server sends READY
client sends AUTH username password
server sends OK
client sends commands
client sends QUIT
server closes
```

Before authentication, only `AUTH` is allowed.

After authentication, more commands are allowed.

This means the server needs a state variable.

```zig
const State = enum {
    waiting_for_auth,
    ready,
    closing,
};
```

Then command handling checks the current state.

```zig
var state: State = .waiting_for_auth;
```

A protocol is not only message formats. It is also the allowed sequence of messages.

#### Errors Are Part of the Protocol

A good protocol defines error replies.

Bad:

```text
something failed
```

Better:

```text
ERR unknown command
ERR invalid argument
ERR not authenticated
ERR message too large
```

Even better for machine clients:

```text
ERR 1001 unknown_command
ERR 1002 invalid_argument
```

Human-readable errors are useful. Stable machine-readable error codes are better for clients.

#### Versioning

Protocols evolve.

Version 1 may support:

```text
PING
ECHO
QUIT
```

Version 2 may add:

```text
TIME
AUTH
```

Versioning can be explicit:

```text
HELLO 1
```

or part of a header:

```text
PROTO/1
```

Without versioning, clients and servers may disagree silently.

For small private tools, this may not matter. For public protocols, it matters a lot.

#### Text Protocols vs Binary Protocols

| Choice | Advantages | Disadvantages |
|---|---|---|
| Text | Easy to debug, easy to type manually, readable logs | Larger, parsing can be slower, escaping rules needed |
| Binary | Compact, fast, exact types | Harder to inspect, requires tools, stricter parsing |

Start with text when learning. Use binary when you need compactness, speed, or exact layout.

#### Security Basics

Network protocols receive untrusted input.

Your server must assume clients may send invalid, huge, slow, or malicious data.

Basic rules:

Set size limits.

Validate all lengths.

Handle malformed input.

Avoid unchecked integer overflow.

Do not trust client-provided paths.

Add timeouts for slow clients.

Close connections that violate the protocol.

Keep error handling explicit.

A network parser should never crash just because the client sent bad bytes.

#### Mental Model

A network protocol is a contract over bytes.

TCP and UDP move data. Your protocol gives that data meaning.

In Zig, you usually model this with slices, parsers, enums, tagged unions, explicit errors, and careful buffer management. Start with a simple text protocol. Once the framing and state rules are clear, the same ideas transfer to larger binary protocols.

