A hands-on collection of small C programs for learning network programming with the Berkeley sockets API. The goal is to build up the fundamentals of TCP socket communication as a stepping stone toward understanding higher-level protocols such as HTTP and MQTT.
| Path | Description |
|---|---|
src/server/server.c |
A TCP echo/chat server that listens on a port, accepts a single client, and exchanges messages interactively. |
src/client/client.c |
A TCP client that connects to a server by hostname and port and sends/receives messages in a loop. |
src/server/Dockerfile |
Multi-stage build for the server image (kisakathejones/tcp-server). |
src/client/Dockerfile |
Multi-stage build for the client image (kisakathejones/tcp-client). |
compose.yaml |
Docker Compose definition that builds/runs both containers on a shared tcpnet bridge network. |
Makefile |
Convenience targets to build and run the server and client natively, and to drive the Docker containers. |
- Creating a socket with
socket(AF_INET, SOCK_STREAM, 0)(IPv4 + TCP) - Server side:
bind(),listen(), andaccept() - Client side: resolving a host with
gethostbyname()andconnect() - Sending and receiving data with
read()/write() - Network byte order conversion with
htons() - Basic error handling via
perror()
- A Unix-like OS (Linux, macOS)
gccandmake
The Makefile uses localhost and port 9999 by default. Start the server first,
then the client — each in its own terminal.
make serverThis compiles src/server/server.c to src/server.out and runs it on the default port.
In a second terminal:
make clientThis compiles src/client/client.c to src/client.out and connects to the server.
Once connected, the client and server take turns exchanging messages:
- The client prompts for a message and sends it.
- The server prints the received message, then prompts for a reply to send back.
Type exit to terminate the session.
The port, hostname, and source files are variables in the Makefile and can be
overridden on the command line:
make server PORT=8080
make client PORT=8080 HOSTNAME=127.0.0.1Remove compiled binaries:
make cleanThe server and the client are packaged as two separate images, each built from its
own Dockerfile:
| Service | Image | Dockerfile |
|---|---|---|
tcp-server |
kisakathejones/tcp-server |
src/server/Dockerfile |
tcp-client |
kisakathejones/tcp-client |
src/client/Dockerfile |
Each image is built in two stages — it compiles its single .c source with gcc, then
ships only the resulting binary on a slim Debian runtime. The ENTRYPOINT is the binary
(./server.out / ./client.out) and the CMD supplies the default arguments, so the
server defaults to listening on port 9999 and the client defaults to connecting to
host server on port 9999.
Both containers are interactive (they read your replies from standard input), so the
services are configured with stdin_open and tty enabled.
compose.yaml builds both images, attaches them to a shared tcpnet bridge network so
the client can reach the server by its service name, and publishes the server's port
9999 to the host. Because the client connects to tcp-server by hostname, no manual
network setup is needed.
Bring the whole stack up in the background:
make silent # sudo docker compose up -dThen attach to each container in its own terminal to interact with it:
make server-attach # attach to the tcp-server container
make client-attach # attach to the tcp-client containerOnce attached, the two containers take turns exchanging messages. Type exit to end
the session.
Tear everything down:
make remove # sudo docker compose downYou can also drive Compose directly if you prefer:
docker compose up -d # build and start both services
docker attach tcp-server # or: docker attach tcp-client
docker compose down # stop and remove the containersTo build from source without Compose:
docker build -t kisakathejones/tcp-server ./src/server
docker build -t kisakathejones/tcp-client ./src/clientCompose already publishes port 9999, so a client outside Docker (for example, the
native make client build or a tool like nc) can reach the containerized server:
make client PORT=9999 HOSTNAME=127.0.0.1- The server currently handles a single client connection at a time (no concurrency).
- Fixed-size buffers (255/256 bytes) are used for simplicity; these examples are for learning and are not hardened for production use.
- Compiled
*.outbinaries are ignored via.gitignore.