Sockets: speaking TCP in C, no framework, no magic
After LKMs and syscalls, let’s stay low. All of Unix networking fits in four system calls, two structs and a treaty of byte order. The rest is memory and discipline. Small memo, Debian Etch required, gcc 4.1 and man pages 2 and 7 as only friends.
The structs, first
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
struct sockaddr_in addr;
addr.sin_family = AF_INET; /* IPv4 */
addr.sin_port = htons(4444); /* network byte order, ALWAYS */
addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 0.0.0.0 */
htons and htonl are not decorative: the network speaks big-endian, your x86 speaks little-endian and the neighbor who forgets htons gets a server on port 45056. man 7 ip explains all this better than me.
The server: socket, bind, listen, accept
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) { perror("socket"); return 1; }
int one = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
listen(s, 16);
for (;;) {
struct sockaddr_in peer; socklen_t plen = sizeof(peer);
int c = accept(s, (struct sockaddr *)&peer, &plen);
if (c < 0) { perror("accept"); continue; }
if (fork() == 0) { /* one process per connection, the routine of the century */
close(s);
dprintf(c, "Bonjour %s\n", inet_ntoa(peer.sin_addr));
_exit(0);
}
close(c); /* the parent closes, or fds leak */
}
Three beginner traps, all paid in debugging: the parent that forgets to close(c) (file descriptors leak until the crash), the server that restarts without SO_REUSEADDR (it refuses to bind for two minutes, the fault of TIME_WAIT) and the fork() without _exit() (the child falls back into the parent’s loop and the server clones itself into a pyramid). My first C server learned this the hard way: three launches, three pyramids and an uptime of fifteen seconds. The fork(2) man page and I eventually learned to respect each other.
The client: socket, connect, done
struct sockaddr_in srv;
srv.sin_family = AF_INET;
srv.sin_port = htons(4444);
inet_aton("192.168.10.11", &srv.sin_addr);
int s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, (struct sockaddr *)&srv, sizeof(srv));
char buf[128];
ssize_t n;
while ((n = read(s, buf, sizeof(buf))) > 0)
write(1, buf, n);
close(s);
read() returns 0 on clean shutdown, -1 on error. Treating that as a detail is the guarantee of a client looping forever on EOF.
What it is worth
Sixty lines, zero dependencies and you have a TCP server. No threads (Etch and its pthreads, I will come back to that), no epoll for now: fork() per connection, which is what inetd did since 1983 and what many services still did. When thousands of connections are needed, we will move to select() then epoll(7); but first, one must know how to hold an accept().
Period evidence, for the archaeologist: the old blog’s RSS feed contains the ls of a dev/sdata/c/ directory on a server of the era and it reads as the complete collection: wsck2-cli-servr.c (the client-server of this article), raw-socket, bof.c, dnstest.c, vmsplice-local-root-exploit.c, stack exercises, shellcode. The server is gone, the files with it. What survives is the know-how rewritten above and, above all, the method: man 2, man 7 and an accept() you only let go of once you understand it.