21. Sockets: a TCP echo server, then a tiny HTTP server
21.1 A TCP echo server (POSIX sockets, the API every language's networking library wraps)
// echo_server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(void) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(9999); // host-to-network byte order — always for wire formats
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 10);
printf("listening on :9999\n");
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &len);
if (client_fd < 0) continue;
char buf[1024];
ssize_t n;
while ((n = read(client_fd, buf, sizeof buf)) > 0) {
write(client_fd, buf, (size_t)n); // echo it straight back
}
close(client_fd);
}
return 0;
}
clang -std=gnu17 -Wall -Wextra -o echo_server echo_server.c # gnu17: BSD sockets are a POSIX
# API, hidden under strict c17
./echo_server &
printf "hello\n" | nc localhost 9999
This one function call, accept(), is the reason the server above can
only handle one client at a time — while it's blocked reading from
client_fd, no other client can connect. Section 23's mini-project fixes
exactly this with select(), and Section 34's Redis walkthrough shows
the production answer (a single-threaded event loop).
21.2 A minimal HTTP/1.0 server on the same socket API
// http_server.c — serves one static string to every request
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(void) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY };
bind(fd, (struct sockaddr *)&addr, sizeof addr);
listen(fd, 16);
printf("http://localhost:8080\n");
while (1) {
int client = accept(fd, NULL, NULL);
char req[2048] = {0};
read(client, req, sizeof req - 1); // real servers must parse Content-Length, not assume one read is the whole request
const char *body = "hello from a C http server\n";
char response[256];
int len = snprintf(response, sizeof response,
"HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n\r\n%s",
strlen(body), body);
write(client, response, (size_t)len);
close(client);
}
}
clang -std=gnu17 -Wall -Wextra -o http_server http_server.c
./http_server & curl -s http://localhost:8080
This is deliberately the simplest possible HTTP server to show the socket API mapping directly onto the protocol you already know from a browser — it's also deliberately not production-ready (single request at a time, no request-size limits, no timeout handling); Section 34's Redis walkthrough and Section 43's capstone show what closes those gaps.