Brief
Six network servers, each graded by Protohackers' live checker hammering a public endpoint you provide, each one rung harder than the last: echo, a JSON protocol, a binary protocol, shared-state chat, a UDP store, and a man-in-the-middle proxy. You write them in Go, one concurrency pattern per rung. The statements below are enough to build against offline; the checker is the final examiner.
Instructions
Rung 0 — Smoke Test (TCP echo)
Echo per RFC 862 semantics: accept TCP connections; for each, send back exactly the bytes received, unmodified, until the client closes its write side (EOF); then flush and close. Handle at least 5 concurrent clients. Pattern: accept loop + goroutine per connection; this skeleton is reused by every later rung.
Rung 1 — Prime Time (line-delimited JSON)
Newline-delimited JSON protocol, one request per line:
{"method":"isPrime","number":<JSON number>} → respond
{"method":"isPrime","prime":<bool>}\n. Rules: method must be exactly "isPrime"
and number a JSON number, else the request is malformed — reply with any malformed
response and disconnect that client. Non-integers (and negatives) are never prime; big
values must not crash you. Multiple requests per connection, in order; concurrent
clients independent. Pattern: bufio.Scanner per connection, validate every field.
Rung 2 — Means to an End (binary, per-connection state)
Fixed 9-byte messages: 1 type byte + two big-endian int32s. I <timestamp> <price>
inserts a price observation. Q <mintime> <maxtime> asks for the arithmetic mean of
prices with mintime ≤ timestamp ≤ maxtime; reply with one big-endian int32 (0 or
anything for an empty/invalid range). Each connection's data is private to it — no
sharing between clients. Pattern: io.ReadFull for exact frames, encoding/binary,
per-connection state only.
Rung 3 — Budget Chat (shared state)
Line-based ASCII chat over TCP (\n-terminated messages):
- On connect, send a name prompt. The first line is the user's name: alphanumeric only, at least 1 character; reject illegal names (message + disconnect).
- On join: announce
* <name> has entered the roomto the others; send the joiner a single line listing present users; relay every subsequent line as[<name>] <msg>to all other users (never back to the sender); on disconnect announce* <name> has left the room. Names are unique while connected. - Solve it twice: once with a mutex-guarded room map, once with a monitor goroutine owning the room and serving joins/leaves/broadcasts over channels. Keep both; form an opinion in a short note.
Rung 4 — Unusual Database Program (UDP)
A key-value store over UDP datagrams (requests under ~1000 bytes):
- A datagram containing
=is an insert: split at the first=; left is key, right (including any further=) is value; later inserts overwrite. Inserts get no response. - A datagram without
=is a retrieve: respond with a single datagramkey=value(current value, or your choice for a missing key). - The key
versionis reserved: retrieving it returns your server's version string; inserts to it are ignored. - No connections, no ordering, possible loss — note where retries/idempotency now sit
with the client. Pattern: single
net.PacketConnloop; state guarded or owned.
Rung 5 — Mob in the Middle (MITM proxy)
A malicious proxy for Budget Chat: listen locally; for each client, dial the upstream
chat server and relay traffic in both directions concurrently, line by line —
rewriting any Boguscoin address in chat messages to Tony's address
(7YWHMfk9JZe0LM0g1ZauHuiSxhI). A Boguscoin address: starts with 7, 26–35
alphanumeric characters, delimited by spaces or the message boundaries. Partial reads
must never split your rewriting — frame on \n before touching the payload. Propagate
either side's disconnect to the other. Pattern: two copy goroutines per session plus
teardown discipline.
Constraints
- Go, standard library only.
- Race-detector-clean rule: every rung's tests pass under
go test -race, and the server runs clean under-racewhile being checked. A data race fails the rung regardless of the checker's verdict. - A rung is complete only when the online checker passes it — local green is necessary, not sufficient — and no rung starts before the previous one passes.
Acceptance
Mapped one-to-one onto the exercise's milestones — for each rung: the Protohackers
online checker passes against your deployed endpoint, go test -race is green, plus the
local recipe:
- Smoke Test —
printf 'hello' | nc host 8000echoes back; 5 parallelncsessions echo independently; EOF closes cleanly. - Prime Time — table-driven tests over primes, composites, non-integers, huge numbers, and malformed JSON; a malformed line disconnects only that client.
- Means to an End — a scripted binary session (inserts then range queries, including empty range) gets correct means; two concurrent clients never see each other's data.
- Budget Chat — a three-terminal
ncsession shows prompt, join/leave announcements, the presence list, and relay-to-others-only; both implementations (mutex and monitor goroutine) pass the same tests; the comparison note exists. - Unusual Database Program —
nc -usession verifies insert silence, retrieve, overwrite, first-=splitting, and the immutableversionkey. - Mob in the Middle — through the proxy, a chat session works end to end and a message containing a Boguscoin address arrives rewritten; addresses at start, middle, and end of message all rewrite; non-matching 7-strings pass untouched.
Related
- Protohackers ladder — the exercise this is the subject of.
- Protohackers — the canonical problem statements and the online checker that grades every rung.
- Tokio mini-redis — subject — the same territory under Rust's async model.