Table of Contents

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:

  1. Listen — the server starts listening on a port (like a phone number).
  2. Connect — the client dials that port.
  3. Accept — the server picks up.
  4. Exchange — both sides read and write data.
  5. 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:

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:

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

  1. Write an echo server that reads lines from a client and sends each line back in uppercase. Hint: convert each character using ASCII math.
  1. Write a "time server" that responds to any connection with the current message "Server uptime: N connections served" (count how many clients have connected).
  1. Write a TCP client that connects to your echo server and sends three messages, printing the responses.
  1. Build a simple HTTP server with three routes: / (home page), /hello?name=X (greeting), and /stats (request count).
  1. 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

Next chapter: Concurrency — threads and channels →

← Previous: Generics Next: Concurrency — threads and channels →