Skip to content

Network Protocol

BrandonRobare edited this page Jun 2, 2026 · 1 revision

Network Protocol

netPong speaks SPPBTP, the line protocol from the course RFC. It is text, not binary. Every message is one line ending in \r\n, and every line starts with a four-character keyword. I chose text on purpose: I could drive the server from telnet, watch the exact bytes on the wire, and debug the handshake by hand. The whole protocol lives in protocol.c and protocol.h.

Transport

The two processes share one TCP connection (AF_INET, SOCK_STREAM). The server binds a port, listens with a backlog of one, and accepts a single client. The client resolves the host with gethostbyname and connects. After accept() the server closes the listening socket, so each launch handles one game.

I send every message with dprintf(fd, "...\r\n", ...), writing straight to the socket fd. I read every message through a buffered stream, fdopen(dup(sock_fd), "r"), so recv_msg() can use fgets to pull one line at a time. The dup keeps the read buffer on its own descriptor, separate from the fd I write through.

Message set

I designed the keyword set to match the RFC. Every keyword is exactly four characters, which makes parsing trivial: read the first token, upper-case four bytes, compare.

Keyword Direction Line format Meaning
HELO server to client HELO 1.0 <ticks> <netheight> <name> server opens; carries protocol version, ticks per second, net height, host name
NAME client to server NAME 1.0 <name> client answers with its name
SERV server to client SERV <n_balls> server announces how many balls the game lasts
BALL either way BALL <net_position> <xttm> <yttm> <ydir> [<char>] the ball crosses the net; carries enough state to continue it on the far court
MISS either way MISS [<text>] sender missed the ball; the point and the next serve move to the receiver
DONE either way DONE [<text>] the last ball is used up; the game is over
QUIT either way QUIT [<text>] sender is leaving, either by pressing Q or to acknowledge DONE
?ERR either way ?ERR [<text>] the sender saw a malformed or unexpected line

recv_msg() parses each line into a tagged struct sppbtp_msg. A blank line returns MSG_NONE and the caller reads again; an unknown keyword also becomes MSG_NONE with the text saved for an error message.

The BALL packet

The BALL line is the only game state that crosses the wire during a rally, so its layout matters most.

BALL <net_position> <xttm> <yttm> <ydir> [<PPBchar>]
Field Type Sender derives it from Receiver uses it for
net_position int ball row minus the top of the play area (y_pos - play_top) the row where the ball enters the local court
xttm int the ball's horizontal time-to-move counter how fast the ball moves left/right locally
yttm int the ball's vertical time-to-move counter how fast the ball moves up/down locally
ydir int the ball's vertical direction (+1 or -1) the up/down direction on the local court
PPBchar char, optional the ball's display symbol the local display symbol; defaults to O if absent

The horizontal direction is not sent. The receiver does not need it, because the ball always enters moving away from its own net edge. Each side stores that as incoming_x_dir in its struct court and applies it locally. That is why one side can leave the field off and the other still continues the ball correctly.

send_ball() leaves PPBchar off when the symbol is the default O, which keeps the common line short. recv_msg() counts how many fields sscanf matched and only treats the fifth as a character when it is present.

What is not synchronized

netPong does not stream the whole board. Two things stay local and never cross the socket:

  • Paddles. Each process draws and moves only its own paddle. Your k/j presses never travel to the opponent. Since the receiving side only needs the ball's entry row and speed to continue play, sending paddle positions would add traffic with no effect on correctness.
  • The ball during a rally. While the ball is on your court, every move is local. The ball crosses the wire exactly once per net-crossing, in a single BALL line, not once per frame.

This keeps the traffic low. A long rally with the ball bouncing on one court for several seconds produces no network messages until the ball reaches the net.

Keeping score in sync

Both processes hold my_score, opponent_score, and a shared balls_left. The counts stay consistent because a miss is deterministic and symmetric.

When I miss locally, bounce_or_lose() decrements balls_left, increments opponent_score, sends MISS, and enters WAIT. When the other side receives that MISS in run_wait_loop(), it decrements its own balls_left, increments its my_score, and either serves the next ball or, if balls_left hit zero, sends DONE. The two balls_left counters start from the same SERV value and step down together, one per miss, so neither side has to send its score over the wire.

Connection setup and a full rally

sequenceDiagram
    participant S as Server (right court)
    participant C as Client (left court)

    Note over S: socket / bind / listen / accept
    C->>S: TCP connect
    S->>C: HELO 1.0 ticks netheight name
    C->>S: NAME 1.0 name
    S->>C: SERV 3

    Note over C: client serves the first ball
    loop one ball in play
        C->>S: BALL net_position xttm yttm ydir [char]
        Note over S: ball simulated in server court
        S->>C: BALL net_position xttm yttm ydir [char]
        Note over C: ball simulated in client court
    end

    C->>S: MISS missed it
    Note over S: score and balls_left update on both sides
    S->>C: DONE good game
    C->>S: QUIT thanks for playing
Loading

One detail surprised me while building this. The server starts the conversation, sending HELO and SERV, but the client serves the first ball. The role that opens the connection is not the role that puts the ball in play. I followed the RFC here rather than the more common "the side that connects acts first" pattern.

Testing the protocol by hand

Because the protocol is text, I tested the server with telnet:

telnet localhost 2001
<- HELO 1.0 20 16 <hostname>
-> NAME 1.0 testbot
<- SERV 3
-> BALL 8 3 4 1 O
...
-> QUIT thanks

A malformed line such as XYZZY makes the server reply ?ERR unknown: XYZZ and close. recv_msg() upper-cases the keyword first, so lower-case input still parses.

Clone this wiki locally