Champ Valeur
Auteur·e Élise
Édition 2024-04-03
Durée 2 sessions
Taille des équipes 1~4 personnes
login des auteurs·rices dans author.txt
Rendu via git, dépôt $YEAR_lab_tcp, droits en lecture à delivery_collector
Compilation lignes de compilation dans justfile : exo1, exo2, exo3, exo4
Binaires N/A

Notation

La réalisation de ce TP ne donne pas une note propre. Cependant, il contribuera à votre note finale de projet sur Clavardage si vous ne le réussissez pas.

Introduction

Aujourd’hui : vous allez découvrir la programmation réseau. Son principe : ajouter la possibilité à deux programmes ou plus de communiquer par internet. C’est un composant essentiel de l’informatique moderne, et vous allez voir qu’une fois une connectés, communiquer avec quelqu’un à distance est aussi simple que d’écrire dans un fichier.

La difficultés de la programmation réseau réside dans la gestion :

  • de plusieurs clients ;
  • d’un nombre variable de client ;
  • des connexions ;
  • des déconnexions ; et
  • des erreurs (sans s’arrêter purement et simplement).

L’ordre du jour :

  • attendre des connexions et les accepter ;
  • communiquer avec le client distant ; et
  • utiliser netcat.

Appels systèmes

Résumé des headers et des pages de manuel dont vous allez avoir besoin :

sys/socket.h: socket, bind, listen, accept, connect
man socket.2 bind.2 listen.2 accept.2 connect.2

netinet/in.h: struct sockaddr_in, INADDR_ANY
man ip.7

arpa/inet.h: htons
man htons.3

sys/socket.h, netinet/in.h, arpa/inet.h: inet_addr
man inet_addr.3

Socket

Aux yeux de vos programmes, un·e socket c’est un descripteur de fichier, mais qui ne fait pas référence à un fichier. Un peu comme 0, 1 et 2. L’appel système socket permet de demander un descripteur de fichier configuré pour un certain usage. Ici on en veut un configuré pour la communication sur internet, en flux, en TCP, donc on va former notre appel ainsi :

int sock = socket(AF_INET, SOCK_STREAM, 0);

AF_INET permet de faire une socket destinée à la communication sur le réseau IP (OSI 3 network). Si on souhaite faire du TCP on indique systématiquement SOCK_STREAM, qui produit une socket sur laquelle on peut read et write comme s’il s’agissait d’un fichier sur notre disque. Le dernier paramètre sert à préciser un protocole à utiliser, sauf qu’en AF_INET et SOCK_STREAM il n’y a qu’un seul protocole possible : TCP, donc on a rien à préciser.

Une fois créée la socket n’est capable de rien. C’est juste un descripteur de fichier sur lequel on ne peut encore ni lire ni écrire.

Pour qu’elle puisse servir à quelque chose on doit :

  • soit la configurer pour nous permettre d’écouter sur un port ;
  • soit la configurer pour nous permettre de parler avec un serveur.

Pour chaque socket AF_INET le noyau Linux garde plusieurs informations en mémoire :

  • son état (pas connecté, à l’écoute, établi, en attente de fermeture, connexion en cours, fermée, etc.) ;
  • une adresse IP et un port source ;
  • une adresse IP et un port destination ;
  • les données en attente d’être lues ou envoyées (les tampons) ;
  • le protocole utilisé.

Bind

L’appel bind permet d’associer une adresse et un port source à une socket. L’information qui nous intéresse le plus ici étant le port source, qui est le port sur lequel on va pouvoir recevoir des connexions.

struct sockaddr_in addr_and_port;

memset(&addr_and_port, 0, sizeof(addr_and_port));
addr_and_port.sin_family = AF_INET;
addr_and_port.sin_addr.s_addr = INADDR_ANY;
addr_and_port.sin_port = htons(1234);
bind(sock, (struct sockaddr *) &addr_and_port, sizeof(addr_and_port));

Listen

Associer une adresse et un port source à une socket n’est pas suffisant pour que les autres programmes et machines aient le droit d’essayer de se connecter à nous. Il faut aussi changer l’état de la socket, de sorte que le noyau sache que nous sommes prêt·e·s et l’écoute pour gérer de nouvelles connexions.

listen(sock, 10);

Le nombre en deuxième argument correspond au nombre de connexions pas encore acceptées que le noyau gardera en liste d’attente. Au delà il laissera tomber les connexions.

Accept

Cet appel permet d’accepter une connexion en attente, ou de bloquer tant que personne n’essaie de se connecter.

int new_conn = accept(sock, NULL, 0);
if (new_conn != 1) {
	stu_dprintf(new_conn, "%s\n", "hai!!!!!");
	close(new_conn);
}

accept renvoie une socket spécialement configurée pour la communication avec un client ou -1 en cas d’erreur, auquel cas errno contiendra le code associé à l’erreur.

Exercice : echo echo echo

Écrivez un programme capable d’accepter la connexion d’un client, et de lui répéter tout ce qu’il envoie jusqu’à ce qu’il ferme la connexion. Après quoi votre programme doit quitter.

Votre programme doit recevoir en premier argument le port sur lequel bind.

Vous pouvez vous connecter à un serveur en utilisant netcat :

$ nc -N 127.0.0.1 1234
> hello world
< hello world
> this is me
< this is me
^D
$ 

Exercice : echo à la chaîne

Même exercice que le précédent, excepté qu’à la déconnexion d’un client vous devez en accepter un nouveau. Si le message d’un client commence par /shutdown, faite s’arrêter proprement votre programme.

Exercice : porte à porte

Écrivez un programme qui tente de se bind à un port passé en argument. Si le port n’est pas disponible, il essaie le port suivant, et ainsi de suite jusqu’à ce qu’il parvienne à bind.

Appel système : connect

Du côté du client, on peut se connecter à un serveur en :

  • créant une socket ;
  • utilisant l’appel système connect.
int sock;
struct sockaddr_in dest_addr;

sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
dest_addr.sin_port = htons(1234);
if (connect(sock, (struct sockaddr *) &dest_addr, sizeof(dest_addr)) == 0) {
	stu_dprintf(sock, "hewwwoo サーバーちゃん!\n");
	// ici on peut parler avec le serveur
} else {
	stu_dprintf(2, "connect: %s\n", strerror(errno));
}

La fonction inet_addr parse une IP donnée dans une chaîne de caractères et produit une valeur valide pour un paquet réseau. La fonction htons prend un unsigned short et le convertit en un une valeur valide pour un paquet réseau également.

Exercice : connect

Écrivez un programme qui prend un argument une adresse IP et un port et qui s’y connecte. Le programme doit successivement :

  • lire sur l’entrée standard et envoyer ce qui a été lu sur la socket ;
  • lire sur la socket et écrire sur la sortie standard ce qui a été lu.

S’il n’y a plus rien à lire sur l’entrée standard ou de la part du serveur : faites s’arrêter le programme.

Vous pouvez tester votre programme avec netcat en le mettant en mode écoute. Exemple avec le port 1234 :

nc -lp 1234

Ré-utilisation du port

Parfois le port qu’utilisait votre programme n’est pas libéré immédiatement. Il est possible, avant de bind, d’utiliser cette fonction :

int sock_configure_reusable_port(int sock)
{
    const int opt = 1;

    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        dprintf(2, "setsockopt(SO_REUSEADDR): %s", strerror(errno));
        close(sock);
        return 1;
    }
    return 0;
}

Appel système : shutdown

Lorsque l’on veut terminer une connexion, on peut la close, mais ça n’est pas toujours suffisant. Pour arrêter proprement une connexion on peut utiliser shutdown.

shutdown(client, SHUT_RDWR);