A single-threaded, non-blocking TCP echo server for Linux. It uses epoll with edge-triggered, one-shot connection events. Each connection reads data into a bounded buffer, writes all of that data back while correctly handling partial writes, then resumes reading.
- Linux with
epollsupport - A C11 compiler and
make
The project does not build natively on macOS because macOS does not provide
epollor<sys/epoll.h>.
makeThe default build enables C11 and useful compiler warnings, producing ./server.
Other targets:
make debug # unoptimized symbols for debugging
make sanitize # AddressSanitizer + UndefinedBehaviorSanitizer
make test # build and run automated Linux integration tests
make cleanThe included multi-stage Dockerfile uses Debian 12 (Bookworm) slim. Debian 12 is a stable, supported Linux release with glibc and native epoll support. The build stage installs only the required build and test dependencies (build-essential, make, and python3) and runs the integration suite. The final runtime image contains only the server binary and runs it as an unprivileged user.
Build the image. This also compiles and runs the Linux integration tests during the build:
docker build --tag async-echo-server .Run the server and publish its TCP port:
docker run --rm --init --publish 8080:8080 async-echo-serverThen, from another terminal:
nc 127.0.0.1 8080To run only the build-and-test stage interactively, use the named build stage:
docker build --target build --tag async-echo-server-test .
docker run --rm async-echo-server-test make test./server [bind-address] [port]Defaults:
- bind address:
127.0.0.1 - port:
8080 - listen backlog:
256 - per-connection echo buffer:
16 KiB
For example, to listen on all IPv4 interfaces at port 9000:
./server 0.0.0.0 9000Connect with netcat:
nc 127.0.0.1 8080Anything sent by the client is echoed back. Stop the server cleanly with Ctrl-C.
- The listener and accepted sockets are non-blocking and close-on-exec.
accept4()accepts connections untilEAGAINto satisfy edge-triggered epoll semantics.- Connections use
EPOLLONESHOT; their interest is explicitly rearmed after each I/O pass. - Reads, writes,
EINTR,EAGAIN, peer EOF, partial writes, and epoll errors are handled explicitly. - When the 16 KiB connection buffer is full, the server switches to writing before reading further. This provides bounded memory use and TCP backpressure.
The automated integration suite requires Linux, Python 3, and a built server:
make testThe suite starts the server on an ephemeral loopback port and verifies:
- small text and binary payloads;
- payloads larger than the 16 KiB connection buffer;
- fragmented client writes;
- 64 concurrent clients;
- client half-closes after sending, while still receiving the full echo, including when the server is blocked writing the echo back;
- clients closing while their echo is still pending do not kill the server with
SIGPIPE; and - running out of file descriptors rejects excess connections instead of stopping the server.
Run it against a separately built executable if needed:
python3 tests/test_echo_server.py --server /path/to/serverFor a quick manual check:
printf 'hello\n' | nc 127.0.0.1 8080For load testing, an echo benchmark such as rust_echo_bench can be used:
cargo run --release -- --address 127.0.0.1:8080 --number 1000 --duration 60 --length 512For memory and undefined-behavior checks, rebuild with make sanitize before running the same workload.