Sockets : parler TCP en C, sans framework ni magie
Après les LKMs et les syscalls, restons en bas. Tout le réseau d’Unix tient dans quatre appels système, deux structures et un traité d’ordre des octets. Le reste, c’est de la mémoire et de la discipline. Petit mémo, Debian Etch oblige, gcc 4.1 et les man pages 2 et 7 pour seuls amis.
Les structures, d’abord
#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, TOUJOURS */
addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 0.0.0.0 */
htons et htonl ne sont pas décoratifs: le réseau parle big-endian, votre x86 parle little-endian et le _getMac du voisin qui oublie htons obtient un serveur sur le port 45056. Le man 7 ip explique tout ça mieux que moi.
Le serveur: 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) { /* un processus par connexion, la routine du siècle */
close(s);
dprintf(c, "Bonjour %s\n", inet_ntoa(peer.sin_addr));
_exit(0);
}
close(c); /* le père ferme, sinon les fds fuient */
}
Trois pièges de débutant, tous payés en debug: le père qui oublie de close(c) (les file descriptors fuient jusqu’au crash), le serveur qui relance sans SO_REUSEADDR (il refuse de binder pendant deux minutes, la faute à TIME_WAIT) et le fork() sans _exit() (le fils repasse dans la boucle du père et le serveur se clone en pyramide). Mon premier serveur C a appris ça à la manière forte: trois lancements, trois pyramides et un uptime de quinze secondes. La page fork(2) et moi avons fini par nous respecter.
Le 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() retourne 0 sur une fermeture propre, -1 sur erreur. Le traiter comme un détail, c’est la garantie d’un client qui boucle à l’infini sur EOF.
Ce que ça vaut
Soixante lignes, zéro dépendance et on tient un serveur TCP. Pas de threads (Etch et ses pthreads, j’y reviendrai), pas d’epoll pour l’instant: fork() par connexion, c’est ce qu’inetd faisait depuis 1983 et ce que beaucoup de services faisaient encore. Quand il faudra des milliers de connexions, on passera à select() puis epoll(7); mais d’abord, il faut savoir tenir un accept().
Une preuve de période, pour l’archéologue: le feed RSS de l’ancien blog contient l’ls d’un répertoire dev/sdata/c/ sur un serveur de l’époque et on y lit la collection complète: wsck2-cli-servr.c (le client-serveur de cet article), raw-socket, bof.c, dnstest.c, vmsplice-local-root-exploit.c, des exercices de pile, du shellcode. Le serveur a disparu, les fichiers avec. Ce qui survit, c’est le savoir-faire réécrit ci-dessus et, surtout, la méthode: man 2, man 7 et un accept() qu’on ne lâche qu’en le comprenant.