Networking — TCP servers
Programs that talk
Every program you have written so far runs alone. It reads files, computes things, and prints output. But most software today is networked — it talks to other programs over the internet.
When you open a website, your browser (a program) sends a request to a server (another program). The server reads the request, processes it, and sends back a response. That conversation happens over TCP (Transmission Control Protocol) — the foundation of the internet.
In this chapter, you will learn to write both sides: clients that send requests and servers that handle them.
TCP basics
TCP works like a phone call:
- Listen — the server starts listening on a port (like a phone number).
- Connect — the client dials that port.
- Accept — the server picks up.
- Exchange — both sides read and write data.
- Close — either side hangs up.
A port is a number (0–65535) that identifies a service on a machine. Web servers typically use port 80 (HTTP) or 443 (HTTPS). For development, we use ports like 8080 or 9000.
Starting a TCP server
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Server listening on port 9000...") let client: int = tcp_accept(server) print("Client connected!") tcp_write(client, "Hello from Nyx!\n") tcp_close(client) tcp_close(server) }
Let's break this down:
tcp_listen("0.0.0.0", 9000)— start listening on all interfaces, port 9000. Returns a server socket (an integer).tcp_accept(server)— wait for a client to connect. This blocks — the program pauses here until someone connects. Returns a client socket.tcp_write(client, ...)— send data to the client.tcp_close(...)— close the connection.
To test this, run the server in one terminal, then in another:
echo "hi" | nc localhost 9000
You will see "Hello from Nyx!" printed by nc.
Reading from a client
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Echo server on port 9000...") let client: int = tcp_accept(server) let message: String = tcp_read_line(client) print("Received: " + message) tcp_write(client, "Echo: " + message + "\n") tcp_close(client) tcp_close(server) }
tcp_read_line(client) reads data from the client until it sees a newline character. This is perfect for text-based protocols.
For reading raw bytes, use tcp_read(client, max_bytes) which reads up to max_bytes bytes.
A server that handles multiple clients
The examples above handle one client and then stop. A real server keeps running:
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Echo server running on port 9000...") while 1 > 0 { let client: int = tcp_accept(server) let line: String = tcp_read_line(client) if line.length() > 0 { print("Got: " + line) tcp_write(client, "Echo: " + line + "\n") } tcp_close(client) } }
This infinite loop accepts a client, reads one line, echoes it back, closes the connection, and waits for the next client. This is a sequential server — it handles one client at a time. (In Chapter 18, you will learn to handle many clients concurrently.)
Writing a TCP client
The other side of the conversation — a program that connects to a server:
fn main() { let sock: int = tcp_connect("127.0.0.1", 9000) if sock < 0 { print("Could not connect") return 0 } tcp_write(sock, "Hello server!\n") let response: String = tcp_read_line(sock) print("Server said: " + response) tcp_close(sock) }
tcp_connect(host, port) connects to a server and returns a socket. If the connection fails, it returns a negative value.
Building an HTTP response by hand
HTTP (the protocol of the web) is built on TCP. An HTTP response is just text with a specific format:
fn http_ok(body: String) -> String { return "HTTP/1.1 200 OK\r\nContent-Length: " + int_to_string(body.length()) + "\r\n\r\n" + body } fn main() { let server: int = tcp_listen("0.0.0.0", 8080) print("HTTP server on http://localhost:8080") while 1 > 0 { let client: int = tcp_accept(server) // Read the request (first line is enough) let request_line: String = tcp_read_line(client) print("Request: " + request_line) // Send a response let response: String = http_ok("<h1>Hello from Nyx!</h1>") tcp_write(client, response) tcp_close(client) } }
Open http://localhost:8080 in a browser and you will see "Hello from Nyx!" — a web page served by your program.
Using the HTTP library
Building HTTP by hand is educational, but Nyx provides a much easier way:
import { http_serve, http_response } from "std/http" fn on_request(request: Array) -> String { let method: String = request[1] let path: String = request[2] if path == "/" { return http_response(200, "Welcome to Nyx!") } if path == "/about" { return http_response(200, "Nyx is a compiled language.") } return http_response(404, "Not Found") } fn main() { print("Server running on http://localhost:8080") http_serve(8080, on_request) }
http_serve handles all the TCP plumbing for you. Your function receives a parsed request array and returns a response string. The request array contains:
request[1]— method (GET, POST, etc.)request[2]— path (/about, /api/data, etc.)request[3]— headers (flat array of key-value pairs)request[4]— body (for POST requests)
DNS resolution
Before connecting to a remote server, you might need to resolve a hostname:
fn main() { let ip: String = resolve("example.com") print("example.com is at " + ip) let sock: int = tcp_connect(ip, 80) tcp_write(sock, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") let response: String = tcp_read(sock, 4096) print(response) tcp_close(sock) }
resolve(hostname) returns the IP address as a string.
Practical example: a key-value server
Let's build a simple server that stores and retrieves values:
fn main() { var store: Map = Map.new() let server: int = tcp_listen("0.0.0.0", 7000) print("KV server on port 7000") while 1 > 0 { let client: int = tcp_accept(server) let line: String = tcp_read_line(client) if line.startsWith("SET ") { let rest: String = line.substring(4, line.length()) let space: int = rest.indexOf(" ") if space > 0 { let key: String = rest.substring(0, space) let value: String = rest.substring(space + 1, rest.length()) store.insert(key, value) tcp_write(client, "OK\n") } } if line.startsWith("GET ") { let key: String = line.substring(4, line.length()) if store.contains(key) { tcp_write(client, store.get(key) + "\n") } else { tcp_write(client, "NOT FOUND\n") } } tcp_close(client) } }
Test it:
echo "SET name Alice" | nc localhost 7000 # OK echo "GET name" | nc localhost 7000 # Alice echo "GET unknown" | nc localhost 7000 # NOT FOUND
Exercises
- Write an echo server that reads lines from a client and sends each line back in uppercase. Hint: convert each character using ASCII math.
- Write a "time server" that responds to any connection with the current message "Server uptime: N connections served" (count how many clients have connected).
- Write a TCP client that connects to your echo server and sends three messages, printing the responses.
- Build a simple HTTP server with three routes:
/(home page),/hello?name=X(greeting), and/stats(request count).
- Build a chat-like server where the server reads lines from a client and responds based on keywords: "hello" → "Hi there!", "bye" → "Goodbye!", anything else → "I don't understand."
Summary
tcp_listen(host, port)starts a TCP server. Returns a server socket.tcp_accept(server)waits for a client. Returns a client socket.tcp_connect(host, port)connects to a server. Returns a socket.tcp_read(sock, max)reads up tomaxbytes.tcp_read_line(sock)reads one line.tcp_write(sock, data)sends data.tcp_close(sock)closes a connection.resolve(hostname)converts a hostname to an IP address.- HTTP is text over TCP. Nyx's
http_servehandles the protocol for you. - Sequential servers handle one client at a time — concurrency comes next.
Next chapter: Concurrency — threads and channels →