Chapter 15: Networking and HTTP
C programs can talk across machines by using sockets. A socket is an operating system handle that represents one endpoint of a network conversation. In this chapter you will learn how to open sockets, how to exchange bytes over TCP, how HTTP sits on top, and how to serve tiny REST like endpoints. The focus stays on portable POSIX style code that compiles on Linux and macOS, with short notes where Windows needs a change.
man 2 socket, man 2 connect, man 2 bind, man 2 listen, man 2 accept, and man 3 getaddrinfo.
Sockets and basic network communication
A socket is created with socket(). You specify a family, a type, and a protocol. For TCP over IPv4 or IPv6 you use AF_INET or AF_INET6, SOCK_STREAM, and protocol zero. Addresses are described with structures like struct sockaddr_in and struct sockaddr_in6, which are passed to system calls by casting to struct sockaddr *. Host and network byte order can differ, so you convert with htons() and htonl() when filling port and address fields.
Creating and configuring a socket
Clients usually call socket() then connect(). Servers call socket(), bind(), listen(), then loop on accept(). For portability and DNS resolution you use getaddrinfo() to obtain one or more sockaddr results that you can try in order.
// minimal socket creation snippet
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) { perror("socket"); return 1; }
// optional: set SO_REUSEADDR for quick restarts
int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1) {
perror("setsockopt");
}
Resolving names
getaddrinfo() turns a host name and service string into a linked list of usable addresses. You iterate that list and try to connect or bind until one succeeds.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int connect_to(const char *host, const char *service) {
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
int rc = getaddrinfo(host, service, &hints, &res);
if (rc != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc)); return -1; }
int fd = -1;
for (p = res; p; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1) continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd; // connected or -1
}
WSAStartup() before socket functions, use closesocket() instead of close(), and link with Ws2_32.lib. Function names are similar but not identical.
Sending and receiving bytes
Use send() and recv() for TCP streams. Both can return fewer bytes than requested, so you loop until all data is processed or an error occurs. A return of zero from recv() means the peer closed the connection cleanly.
ssize_t send_all(int fd, const void *buf, size_t len) {
const char *p = buf;
size_t left = len;
while (left > 0) {
ssize_t n = send(fd, p, left, 0);
if (n <= 0) return n; // error or closed
p += n;
left -= n;
}
return len;
}
Creating a TCP client and server
This section builds a tiny echo server and client. The server accepts a connection, reads some bytes, writes them back, then closes. The client connects, sends a line, reads the response, and prints it.
Writing a simple echo server
The server runs on a port, for example 8080. It uses getaddrinfo() with AI_PASSIVE to obtain a suitable local address for bind(), then listens and accepts. This example handles one client at a time to keep the flow clear.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, "8080", &hints, &res) != 0) { perror("getaddrinfo"); return 1; }
int fd = -1;
for (p = res; p; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1) continue;
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
if (bind(fd, p->ai_addr, p->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd == -1) { fprintf(stderr, "bind failed\n"); return 1; }
if (listen(fd, 16) == -1) { perror("listen"); return 1; }
printf("Echo server listening on port 8080\n");
for (;;) {
int cfd = accept(fd, NULL, NULL);
if (cfd == -1) { perror("accept"); continue;
}
char buf[1024];
ssize_t n = recv(cfd, buf, sizeof buf, 0);
if (n > 0) {
send(cfd, buf, (size_t)n, 0);
}
close(cfd);
}
}
select() or poll(). Start with the simple loop, then evolve as requirements grow.
Writing a simple echo client
The client connects to localhost:8080, sends a short message, waits for a reply, and prints it to standard output.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
struct addrinfo hints, *res;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("127.0.0.1", "8080", &hints, &res) != 0) {
perror("getaddrinfo"); return 1;
}
int fd = -1;
for (struct addrinfo *p = res; p; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1) continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) { break; }
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd == -1) { fprintf(stderr, "connect failed\n"); return 1; }
const char *msg = "hello\n";
send(fd, msg, strlen(msg), 0);
char buf[1024];
ssize_t n = recv(fd, buf, sizeof buf - 1, 0);
if (n > 0) { buf[n] = '\0'; printf("got: %s", buf); }
close(fd);
return 0;
}
HTTP request-response basics in pure C
HTTP rides on TCP. The client opens a TCP connection to port 80 or 443 then sends a text request. The server reads the request line and headers, then writes back a status line, headers, and a body. You can experiment with a raw socket to see the wire format clearly.
Composing an HTTP GET by hand
This example connects to an origin server and sends a minimal HTTP 1.1 request. The host name must appear in a Host header. The connection may remain open, so you can close it after reading or include Connection: close to make life simple.
// show the literal request that will be sent
// lines end with CRLF in the protocol
GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: this-is-c/1.0\r\n
Accept: */*\r\n
Connection: close\r\n
\r\n
// tiny HTTP GET client
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
struct addrinfo hints, *res;
memset(&hints, 0, sizeof hints);
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("example.com", "80", &hints, &res) != 0) {
perror("getaddrinfo"); return 1;
}
int fd = -1;
for (struct addrinfo *p = res; p; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1) continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd == -1) { fprintf(stderr, "connect failed\n"); return 1; }
const char *req =
"GET / HTTP/1.1\r\n"
"Host: example.com\r\n"
"User-Agent: this-is-c/1.0\r\n"
"Accept: */*\r\n"
"Connection: close\r\n"
"\r\n";
send(fd, req, strlen(req), 0);
char buf[4096];
ssize_t n;
while ((n = recv(fd, buf, sizeof buf, 0)) > 0) {
fwrite(buf, 1, (size_t)n, stdout);
}
close(fd);
return 0;
}
Parsing a minimal HTTP request on the server
To serve HTTP you need to read until you reach the blank line that ends the headers. For a first cut you can split on \r\n\r\n then parse the request line. Production servers need robust parsers and careful limits.
#include <ctype.h>
// return pointer just after CRLFCRLF or NULL if not found
static const char *find_header_end(const char *s, size_t n) {
for (size_t i = 0; i + 3 < n; ++i) {
if (s[i] == '\r' && s[i+1] == '\n' && s[i+2] == '\r' && s[i+3] == '\n')
return s + i + 4;
}
return NULL;
}
Building minimal REST-like endpoints
A REST like endpoint returns a representation of a resource for a path. You can build a tiny server that maps paths such as /time or /echo?x=... to handler functions. The example below handles two endpoints and returns JSON. It avoids a full query string parser for clarity, and it keeps responses small.
Serving simple JSON from path based handlers
Here is a compact server that accepts a connection, reads at most one request, picks a handler by prefix, and returns a JSON body with a correct Content-Type header. Error handling is trimmed to essentials to focus on the control flow.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
static void send_response(int cfd, int status, const char *status_text, const char *body) f
char hdr[512];
int blen = (int)strlen(body);
int n = snprintf(hdr, sizeof hdr,
"HTTP/1.1 %d %s\r\n"
"Content-Type: application/json; charset=utf-8\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n", status, status_text, blen);
send(cfd, hdr, (size_t)n, 0);
send(cfd, body, (size_t)blen, 0);
}
int main(void) {
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, "8081", &hints, &res) != 0) {
perror("getaddrinfo"); return 1;
}
int fd = -1;
for (p = res; p; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd == -1) continue;
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
if (bind(fd, p->ai_addr, p->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd == -1) { fprintf(stderr, "bind failed\n"); return 1; }
if (listen(fd, 16) == -1) { perror("listen"); return 1; }
printf("REST like server on http://127.0.0.1:8081/\n");
for (;;) {
int cfd = accept(fd, NULL, NULL);
if (cfd == -1) { perror("accept"); continue;
}
char req[4096];
ssize_t n = recv(cfd, req, sizeof req - 1, 0);
if (n <= 0) { close(cfd); continue; }
req[n] = '\0';
// parse the request line method and path
char method[8], path[1024];
method[0] = path[0] = '\0';
sscanf(req, "%7s %1023s", method, path);
if (strcmp(method, "GET") != 0) {
send_response(cfd, 405, "Method Not Allowed", "{ \"error\": \"only GET\" }");
close(cfd);
continue;
}
if (strcmp(path, "/time") == 0) {
char body[256];
time_t t = time(NULL);
struct tm tmv;
gmtime_r(&t, &tmv);
char iso[64];
strftime(iso, sizeof iso, "%Y-%m-%dT%H:%M:%SZ", &tmv);
snprintf(body, sizeof body, "{ \"utc\": \"%s\" }", iso);
send_response(cfd, 200, "OK", body);
}
else if (strncmp(path, "/echo", 5) == 0)
{
// naive echo of the path for demonstration
char body[512];
snprintf(body, sizeof body, "{ \"path\": \"%s\" }", path);
send_response(cfd, 200, "OK", body);
} else
{
send_response(cfd, 404, "Not Found", "{ \"error\": \"not found\" }");
}
close(cfd);
}
return 0;
}
name=value pairs separated by &. Percent decoding turns %20 into a space. Keep buffers bounded and reject lines longer than your limit.
Adding minimal concurrency
To handle more than one client, you can accept in a loop and create a thread per connection. The function below sketches a simple thread wrapper. On POSIX you can use pthread_create(). On systems without pthreads you can use a small worker loop with select() that watches many sockets.
#include <pthread.h>
struct client_arg { int fd; };
static void *serve(void *argp) {
int cfd = ((struct client_arg*)argp)->fd;
free(argp);
// handle one request then close (reuse the handler shown above)
// … copy the read and route logic here …
close(cfd);
return NULL;
}
// inside the accept loop
int cfd = accept(fd, NULL, NULL);
if (cfd != -1) {
struct client_arg *a = malloc(sizeof *a);
a->fd = cfd;
pthread_t tid;
pthread_create(&tid, NULL, serve, a);
pthread_detach(tid);
}
poll() or epoll for high concurrency once the design is proven.