Accueil > Programmation Cocoa > Réseau et API de Sockets BSD

Réseau et API de Sockets BSD

Par Mike Beam le 27/12/2002

Traduit par Olivier, le 06/02/2003

Nous avons passé beaucoup de temps à parler à propos de RendezVous dans les deux articles précédents, mais RendezVous n’a pas été créé de rien. Avoir un service de détection facile d’utilisation ne nous sert à rien si nous ne pouvons pas faire communiquer nos applications. En effet, RendezVous ne facilite aucunement la communication réseau entre applications, car il s’agit seulement d’un protocole réseau de publication et de détection de services.C’est un protocole de détection, pas un protocole de communication.

Aujourd’hui, nous allons voir l’aspect communication de l’affaire ; nous aurons peu de choses à dire à propos de RendezVous.
De par son héritage Unix, Mac OS X est une plate-forme merveilleuse pour apprendre le réseau, parce qu’il possède une API très riche ; en particulier nous pouvons programmer avec l’API des vénérables sockets BSD.
Aujourd’hui, nous étudierons cette API, et pour cela, nous écrirons deux petites applications en C qui nous montrerons comment clients et serveurs peuvent communiquer.
Dans le prochain article, nous finirons RCE (NDT : RendezVous Chat Example, voir articles précédents sur RendezVous) avec ce que nous avons appris aujourd’hui en y ajoutant un peu de cocoa.

A propos des Sockets

La plupart d’entre nous avons entendu parler de sockets au cours de notre vie de programmeur.
Pour ma part, j’ai toujours entendu parler des sockets, mais jusqu’il y a environ six mois, je n’avais pas eu le plaisir d’en programmer.
Entendre parler de quelque chose ne signifie pas le comprendre, ce qui est essentiel pour pouvoir utiliser efficacement une technologie.
Dans cet article et le prochain, j’espère susciter un peu d’intérêt sur ce sujet pour vous faire apprécier cette technologie.
En espérant que ceux qui jusque là avaient évité les sockets et le réseau poursuivront et en apprendront plus sur ce sujet intéressant.

Mais qu’est-ce-qu’une socket ? La page man pour la fonction socket(), que nous utilisons pour créer des sockets, les décrit en quatre mots : un point de communication.
L’analogie la plus utilisée pour décrire les sockets est celle du téléphone, qui comme nous le verrons, est en effet très bien choisie.
Un téléphone est après tout un point ou une interface de communication réseau que nous utilisons pour communiquer avec d’autres personnes.

De la même façon que nous parlons et écoutons à l’appareil, les applications envoient des données à travers le réseau en écrivant sur une socket, et reçoivent des données envoyées par un serveur distant en lisant sur la socket.
Si vous êtes familier avec l’API Unix de lecture et écriture de fichier, vous serez à l’aise avec les sockets, car les mêmes fonctions d’Entrée/Sortie sont utilisées pour les sockets — soient, read() et write().

Comme deux téléphones permettent la communication entre deux personnes, les connexions réseaux existent entre une paire de sockets, une pour chaque bout de la connexion.
On parle souvent des sockets par paires : une pour l’application serveur, et une pour l’application cliente.
Le modèle réseau auquel nous sommes accoutumés est celui d’une relation entre en serveur et un client.
Un serveur est une application qui est à l’écoute de requêtes de connexions de clients, et les gère de façon appropriée.
Un client est un programme qui se connecte à un serveur.
En général, client et serveur sont deux applications bien distinctes, comme c’est le cas pour un client et un serveur internet : Apache est un serveur web, tandis qu’OmniWeb, Internet Explorer et Mozilla sont tous des clients web.

Nous verrons dans cet article comment cette distinction entre serveur et client devient floue quand nous parlons d’applications de chat en mode point-à-point comme RCE.
Parfois une application est à la fois un client et un serveur qui accepte des connexions depuis d’autres applications.
C’est particulièrement vrai pour les applications point-à-point telle que l’application de chat que nous sommes en train de construire.
Nous y reviendrons plus dans le prochain article, mais comprenez à mesure que nous avançons dans notre discussion aujourd’hui que RCE aura des fonctionnalités de serveur et de client.

Travailler avec les Sockets

De par les tâches différentes dévolues à un serveur et à un client, leur utilisation des sockets est différente.
Le rôle de chacun lors de l’établissement de la communication est reflété par la nature des sockets utilisées par chaque côté.
A savoir que les serveurs utilisent des sockets passives et le clients des sockets actives.

Quand un processus serveur démarre, il doit créer une socket ; la lier à un port local non utilisé ; indiquer à cette socket de se mettre à l’écoute de demandes de connexions de la part des clients ; et finalement, commencer à attendre de nouvelles connexions.
Cette socket est souvent appelée la socket d’écoute (listening socket), la socket passive, ou la socket serveur.
Tous ces noms suggèrent que le rôle de la socket et d’attendre patiemment à l’écoute de son port des requêtes de connexions de clients.
Dans l’analogie du téléphone, créer une socket est comme acheter un téléphone, lier est comme avoir une connexion à la companie de téléphone, écouter est comme brancher votre téléphone à la prise, et enfin, accepter est comme décrocher le téléphone lorsqu’il sonne.

Quand une demande de connexion est reçue par la socket d’écoute, le serveur doit accepter la connexion et renvoyer une nouvelle socket de connexion qui est utilisée pour communiquer avec le client.
Cette nouvelle socket a une connexion établie avec la socket distante du client.
En créant une nouvelle socket pour gérer la nouvelle connexion, la socket d’écoute est libre de continuer ce qu’elle faisait, être à l’écoute de demandes de connexions d’autres clients.

Les client utilisent une socket de plusieurs façons.
Un client crée une socket de la même manière que le serveur ; cependant, après la création de la socket, son utilisation diffère.
Le client utilise sa socket pour essayer de se connecter au serveur.
Une fois que la connexion a été acceptée, le client commence à envoyer et recevoir des données depuis le serveur.
Pour revenir à notre analogie du téléphone, la connexion est semblable à composer le numéro de la personne à qui vous voulez parler.

Notre Boîte à Outil pour Sockets

Que sont toutes ces fonctions que nous avons évoquées sans vraiment les mentionner ? Ce sont les fonctions de l’API de sockets BSD, qui est principalement définie dans le fichier d’en-tête sys/socket.h (les chemins d’accès des fichiers d’en-tête sont toujours relatifs au répertoire /usr/include).
Il existe sept fonctions que nous allons étudier, dont trois faisant partie de la librairie standard.
Ce sont :

int socket( int domain, int type, int protocol )
  • Crée une nouvelle socket et retourne une descripteur de socket. L’argument domain est une constante pour spécifier la famille d’adresse de la socket ; nous utiliserons AF_INET, qui correspond à l’adressage IPv4.
    L’argument type spécifie le type de socket ; nous passerons la constante SOCK_STREAM ici, qui est une socket stream TCP.
    L’argument protocol ne nous concerne pas ici, donc nous passons 0. Retourne -1 en cas d’erreur.
int connect( int s, const struct sockaddr *name, int namelen )
  • Connecte la socket référencée par le descripteur s à la socket distante spécifiée dans la structure d’adresse name.
    Retourne 0 en cas de succès, -1 en cas d’erreur.
int bind(  int s, const struct sockaddr *name, int namelen )
  • Lie la socket s au port spécifié dans la structure d’adresse name.
    Retourne 0 en cas de succès, -1 en cas d’erreur.
int listen( int s, int backlog )
  • Convertie la socket s en une socket passive (serveur).
    Le paramètre backlog indique combien de connexions en attente le noyau acceptera avant que les clients qui tentent de se connecter reçoivent une erreur connexion refusée.
    Retourne 0 en cas de succès, -1 en cas d’erreur.
int accept( int s, struct sockaddr *addr, int *addrlen )
  • Cette fonction retourne une socket connectée à la socket distante à la première requête de connexion dans la file d’attente de connexion.
    La socket retournée n’est pas la même socket que s, mais possède les mêmes propriétés que s.
    La structure d’adresse de la socket connectée est retournée dans la structure addr.
    Retourne -1 en cas d’erreur.
ssize_t read( int d, void *buf, size_t nbytes )
  • Tente de lire nbytes de données depuis la socket d vers le tableau buf.
    Retourne le nombre d’octets (bytes) réellement lus.
ssize_t write( int d, const void *buf, size_t nbytes )
  • Cette fonction écrit sur la socket d nbytes octets depuis le tableau buf.
int close(  int s )
  • Cette fonction ferme la socket.

Prenons un instant pour étudier ces fonctions.
Nous avons vu plus haut que les serveurs et clients utilisent les sockets de façons différentes.
Ainsi, certaines de ces fonctions ne sont appropriées que pour le client, et d’autres que pour le serveur.
Premièrement, clients et serveurs utilisent socket(), read(), write() et close().
La fonction connect() est utilisée par les clients, tandis que les trois fonctions restantes — bind(), listen() et accept() — sont utilisées par les serveurs.

Notre Programme du Jour

Aujourd’hui nous allons créer deux petites applications en C.
Nous n’allons rien apprendre de nouveau en Cocoa aujourd’hui.
Pour écrire une application C, vous avez deux options.
La première est de créer un nouveau projet dans Project Builder pour l’application cliente, et un autre projet pour l’application serveur.
Le type de ces projets à créer est Standard Tool (Outil/Utilitaire standard), qui est le dernier élément de la liste de l’Assistant Nouveau Type de Projet.
Quand vous créez un nouveau projet d’utilitaire standard, vous obtenez un simple fichier dans le groupe Sources : main.c.
Ce fichier contient du code qui suffit pour effectuer “Hello World!” — nous pouvons remplacer tout cela par notre propre code (même la fonction main et les #include).
Si vous codez aujourd’hui, rappelez-vous que vous devez créer un projet pour le client et un pour le serveur, puisque nous avons besoin de deux exécutables.

L’autre alternative est de coder depuis le Terminal, ce que j’ai fait quand j’ai écrit le code pour cet article.
Dans ce cas, il vous suffit de créer deux fichiers, server.c et client.c et d’utiliser la commande cc.
Une fois que nous aurons obtenu les deux parties nécessaires, je vous montrerai comment compiler le code depuis le shell Unix et depuis Project Builder.
Dans chaque section où je présente le code client et serveur, je passerai en revue les étapes que nous avons à coder de manière isolée, et à la fin, je présenterai le code source en entier pour le composant (client ou serveur).

Et avec tout cela, en avant !

Les Clients

Les clients utilisent les sockets de la manière suivante : premièrement
une application client doit créer une socket, ce qui se fait en utilisant
la fonction socket(), comme montré ici :

int sockfd

if ( (sockfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
   perror( "socket" );
   exit(1);
}

Ceci va créer une socket de streaming TCP/IP.
Une socket de streaming TCP est un moyen de communication très fiable qui comporte toutes sortes de vérification d’erreurs intégrées au protocole, et une interface pratique, permettant d’échanger des octets.
La fonction socket() prend trois arguments : domain, type et protocol.
Avec le paramètre domain nous indiquons le domaine de communication de la socket, qui est basé sur une famille d’adresse particulière.
La constante AF_INET que nous utilisons ici indique à la fonction socket() que nous travaillerons avec les adresses IPv4.
Le second argument, type, indique le type de socket que nous voulons. SOCK_STREAM indique que nous voulons une socket de streaming TCP.
Il existe d’autres types comme les sockets datagram et raw (brutes).
Enfin, nous avons l’argument protocol.
Cet argument n’est pas très utilisé, puisque chaque combinaison de domain et type ne supporte seulement qu’un protocole.
Donc nous passons 0 et le laissons comme cela.

Remarquez comment nous gérons l’erreur dans la fonction socket().
Si un appel à socket() est réussi, il retournera un descripteur pour la nouvelle socket qui est un petit entier positif.
Cependant, si l’appel à socket() est un échec, -1 est retourné.
Nous pouvons vérifier si la valeur de sockfd est positive ou négative dans le bloc if, comme montré ci-dessus.
S’il y a une erreur, nous la gérons en imprimant le message d’erreur en faisant appel à perror(), et nous quittons l’application avec un code retour de 1 (1 indique au processus parent, normalement un shell comme tcsh, qu’il y a eu une erreur lors de l’exécution du programme. Pour plus d’information sur perror(), consultez la page man de perror en tapant man perror dans le Terminal).

Avec la socket obtenue, nous appelons la fonction connect(), qui tentera de créer une connexion entre la socket spécifiée et une socket d’écoute du serveur.
Dans la liste des fonctions ci-dessus, nous avons vu que le second argument de la fonction connect() est de type struct sockaddr, qui est également utilisé dans les fonctions bind() et accept().
Chose intéressante, nous n’avons jamais travaillé avec struct sockaddr.
sockaddr est une sorte de superclass abstraite pour protocoles et une structure d’adresse pour les types de familles des sockets.
En d’autres termes, nous ne travaillons jamais directement avec une structure sockaddr, mais plutôt avec une structure d’adresse pour adresses IPv4 ou IPv6.

Pour IPv4, la structure d’adresse que nous utilisons est de type struct sockaddr_in. La principale raison pour l’existence de structures d’adresses de type générique est que quand l’API socket fut écrite pour la première fois, le C ANSI n’avait pas encore spécifié void * en
tant que pointeur de type générique. Pourtant, les sockets devaient supporter de nombreuses familles d’adresses, et l’idée d’avoir un ensemble séparé de fonctions pour chaque domaine n’était pas très bien vu par les développeurs d’API. Pour s’en sortir, les auteurs de l’API des sockets ont dû créer leur propre type de pointeur générique spécialement pour les structures d’adresses de sockets : struct sockaddr. Cette petite histoire de côté, la définition de struct sockaddr_in se trouve dans le fichier d’en-tête netinet/in.h (quand vous utilisez l’API des sockets avec IPv4, vous devez inclure cet en-tête en plus de sys/sockets.h). La définition de struct sockaddr_in est la suivante :

struct sockaddr_in {
   u_char  sin_len;
   u_char  sin_family;
   u_short sin_port;
   struct  in_addr sin_addr;
   char    sin_zero[8];
};

Le premier membre de la structure, u_char sin_len, est la taille de la structure en octets.
Le type u_char est un synonyme de unsigned char ; les définitions de types pour les types d’usage courant se trouve dans le fichier d’en-tête sys/types.h.
A part des cas spéciaux d’utilisation de sockets, nous n’avons pas besoin d’examiner la valeur du membre sin_len ; il est utilisé en interne par le noyau par différentes routines de sockets.
Le second membre de sockaddr_in est sin_family, qui est la famille d’adresse de la socket.
Ce membre est initialisé à la même constante que nous avons passée comme argument protocol de la fonction socket() (AF_INET par exemple).
Le prochain membre, sin_port, est le numéro du port auquel nous souhaitons nous connecter.
Notre prochain membre, sin_addr, est lui-même une structure de type struct in_addr.
Ensuite nous avons une autre struct, in_addr.
Cette structure ressemble à (et vous penserez peut-être que c’est un peu bête) :


struct in_addr {
   in_addr_t s_addr;
};

L’unique membre de cette structure, s_addr est le type in_addr_t.
La définition de ce type se trouve également dans sys/types.h, dans lequel nous pouvons voir que in_addr_t est un entier non signé de 32 bits, ce qui est en général typé par unsigned int.
Quand nous initialisons notre structure adresse de socket, c’est là que nous stockons l’adresse IP sur laquelle nous voulons nous lier ou tenter de nous connecter.
Enfin, revenant à sockaddr_in, le dernier membre est un simple tableau qui permet de combler la structure pour atteindre une certaine taille.
Nous devons juste nous assurer qu’il est initialisé à 0.

Maintenant que nous avons pris connaissance du contenu de la structure adresse d’une socket, nous pouvons continuer sur le code de notre client pour sa connexion au serveur.
Avant de pouvoir utiliser la fonction connect(), nous devons préparer une structure d’adresse afin que la fonction sache où se connecter.
Les structures d’adresses de socket sont préparées en mettant à zéro la zone mémoire de la structure, et ensuite on initialise les champs nécessaires comme ici :


bzero( &serverAddress, sizeof(serverAddress) );
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons( 12345 );

inet_pton( AF_INET, "127.0.0.1", &serverAddress.sin_addr );

Il y a plusieurs choses à noter ici.
La première chose que nous faisons est d’initialiser complètement la structure avec des 0 en utilisant la fonction bzero(), qui est le raccourci pour byte zero.
Cette fonction prend en argument l’adresse mémoire de départ et le nombre d’octets à mettre à zéro.
Dans notre cas, nous passons l’adresse de la variable serverAddress, obtenue en utilisant l’opérateur d’adresse &, et nous passons le résultat de la macro sizeof() avec serverAddress pour paramètre (sizeof est utilisée très fréquemment en C pour déterminer dynamiquement la taille en octets d’une variable ou d’un type de donnée).

Ensuite, nous initialisons sin_family à AF_INET : la même famille d’adresse avec laquelle nous avons créé notre socket.
Puis nous initialisons le numéro de port sin_port à 12345.
Quand nous nous occuperons du code serveur, nous verrons que nous utiliserons 12345 comme numéro de port pour lier notre socket d’écoute.

Remarquez l’utilisation de la fonction htons().
Cette fonction signifie host to network short et elle est utilisée pour convertir un short int de l’ordre binaire du serveur (little- ou big-endian) vers l’ordre binaire du réseau (big-endian).
Si vous ne connaissez pas ces problèmes d’ordre binaire, en voici une petite explication.
Continuons avec notre exemple du short int : un short int fait 16 bits de long soient 2 octets.
Les ordinateurs d’architectures différentes stockent et lisent en mémoire les valeurs des types de données multi-octets de façons différentes.
Si vous scannez une mémoire et tombez sur une variable short int, vous aurez soit l’octet de poids faible (little end), soit l’octet de poids fort (big end) en premier.
Dans les systèmes little-endian, vous trouverez l’octet de poids faible en premier, tandis que dans les systèmes big-endian, vous trouverez l’octet de poids fort en premier.

L’utilisation de htons() et d’autres fonctions similaires est nécessaire parce que toutes les plates-formes n’ordonnent pas les octets d’un type de données multi-octets (comme un short int, 2 octets) de la même façon.
Nous verrons plus tard une autre variante de cette fonction, htonl()host to network long.

Enfin, nous initialisons l’adresse à laquelle nous voulons nous connecter.
Dans l’objectif de cette article, nous connecterons notre socket au serveur de socket lié au port 12345 sur le serveur local (localhost), dont l’adresse est 127.0.0.1.
La fonction inet_pton() est une fonction pratique pour convertir une chaîne de caractères en une structure in_addr.
Le nom de la fonction est un raccourci pour Internet Presentation to Number (Représentation Internet d’un Nombre).
Presentation signifie une représentation lisible pour l’homme d’une adresse IP, tandis que Number fait référence à la représentation d’un entier d’une adresse IP requise par in_addr struct.
En spécifiant AF_INET dans le premier argument, nous indiquons à la fonction que nous travaillons avec des adresses IPv4.

Connectons nous.
Pour nous connecter à la socket distante, nous utilisons le code suivant
:

if ( connect( sockfd, (struct sockaddr *)&serverAddress,
               sizeof(serverAddress)) < 0 ) {
   perror( "connect" );
   exit(1);
}

Si connect() retourne avec succès, alors nous pouvons commencer à communiquer avec le serveur. Dans notre exemple simple de client, nous ne ferons rien de plus que de lire un message court envoyé par le serveur quand il reçoit une connexion, ce que nous faisons par la fonction read() :

char buffer[201];
int n;

while ( (n = read( sockfd, buffer, 200 )) > 0 ) {
   buffer[n] = 0;	//chaîne de caractère terminé par un caractère null
   printf( buffer );
}

if ( n < 0 ) {
   perror( "read" );
   exit(1);
}

A cause de la nature relativement lente des connexions réseaux, une application cliente n’est pas assurée de recevoir le message envoyé par le serveur dans son intégrité dans le temps écoulé entre connect() et le premier appel à read().
C’est pourquoi nous plaçons read() dans une boucle while et vérifions la valeur retournée par read() à chaque passage pour voir combien d’octets nous avons lus, et continuons la lecture jusqu’à ce qu’il n’y ait plus rien à lire (d’autres données seront reçues entre chaque appel à read()).
A l’intérieur de la boucle, nous intialisons le nième caractère de buffer à null et affichons la chaîne de caractère obtenue jusque là.
Le tableau buffer a été créé d’une taille de 201 octets pour faire de la place au caractère null au cas où la fonction read recevrait 200 caractères en une passe.
Comme nous initialisons le caractère suivant le dernier lu à null, printf() imprimera uniquement les caractère lus au dernier appel de read().
Les fonctions qui travaillent avec les tableaux de caractères en tant que chaînes de caractères cherchent toujours un caractère null qui démarque la fin de la chaîne, ce qui leur évite d’accéder des zones mémoires qu’ils ne devraient pas.

Voyons maintenant le code de client au complet, et nous verrons ensuite comment créer une très simple serveur pour notre client.
Voici le contenu de client.c :

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main( int argc, char **argv )
{
   int n, sockfd;
   char buffer[201];
   struct sockaddr_in serverAddress;

   if ( (sockfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
      perror( "socket" );
      exit(1);
   } 

   bzero( &serverAddress, sizeof(serverAddress) );
   serverAddress.sin_family = AF_INET;
   serverAddress.sin_port = htons( 12345 );

   inet_pton( AF_INET, "127.0.0.1", &serverAddress.sin_addr );

   if ( connect( sockfd, (struct sockaddr *)&serverAddress,
                  sizeof(serverAddress)) < 0 ) {
      perror( "connect" );
      exit(1);
   }

   while ( n = read( sockfd, buffer, 200) ) {
      buffer[n] = 0;
      printf( buffer );
   }

   if ( n < 0 ) {
      perror( "read" );
      exit(1);
   }

   return 0;
}

Les Serveurs

Maintenant nous allons discuter de la procédure de démarrage d’un serveur, qui est un peu plus compliquée que ce qu’un client doit effectuer. La première étape est la même, créer une socket :

struct sockaddr_in serverAddress;
int listenfd, connectfd;

if ( (listenfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
   perror( "socket" );
   exit(1);
}

La seule chose qui a changé ici est que nous avons déclaré une seconde variable de descripteur de fichier de socket pour récupérer la valeur de retour de la fonction accept(), et nous avons changé le nom du descripteur de fichier de la socket d’origine de sockfd en listenfd, pour répercuter la modification de la nature de la socket dans une application serveur.

Ensuite, nous lions la socket à un port et une adresse en utilisant la fonction bind(). La fonction accepte sockaddr_in struct, donc nous devons en préparer une comme nous l’avons fait pour le client :

bzero( &serverAddress, sizeof(serverAddress) );
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons( 12345 );
serverAddress.sin_addr.s_addr = htonl( INADDR_ANY );

L’initialisation de la structure adresse de la socket serveur se fait de la même façon que pour le client, mise à part quelques subtiles différences. D’abord, nous mettons à zéro l’espace mémoire occupé par serverAddress, et ensuite, nous initialisons la famille, le numéro de port, et l’adresse. Notez cependant que nous initialisons les adresses différemment d’auparavant.
Nous aurions pu utiliser inet_pton() avec la même adresse IP du localhost utilisée pour le client, mais le faire limiterait le serveur à n’accepter que des connexions sur l’interface localhost.
En d’autres termes, notre serveur ne pourrait pas accepter de connexions depuis le réseau, puisque nous attendons des connexions que sur l’adresse IP 127.0.0.1.

Il existe des fonctions qui nous permettent d’obtenir une adresse IP de l’interface Ethernet, mais il existe une meilleure solution qui permettra à la socket d’accepter des connexions sur toutes les interfaces disponibles (Ethernet, localhost, Airport, Firewire, etc…).
En initialisant l’adresse à la constante INADDR_ANY, le noyau va lier la socket à ress.sin_addr.s_addr à la représentation réseau (obtenue en utilisant htonl()) de la constante INADDR_ANY toutes les interfaces réseaux disponibles.
Ainsi, si nous avons simultanément une connexion Ethernet active, une connexion Airport active et une interface loopback (127.0.0.1), il sera possible de se connecter à la socket par n’importe quelle interface de ces adresses IP respectives.

Ensuite, nous devons lier la socket à l’adresse spécifiée dans struct serverAddress. Ce que nous faisons par la fonction bind() :

if ( bind( listenfd, (struct sockaddr *)&serverAddress,
            sizeof(serverAddress)) < 0 ) {
   perror( "bind" );
   exit(1);
}

Comme pour la fonction connect(), nous passons le descripteur de fichier de la socket, l’adresse de la structure de socket qui détermine l’adresse et le port auxquels se lier, et enfin, la taille de la structure adresse.
Comme toujours, nous vérifions si la fonction s’exécute avec succès en comparant la valeur retournée à zéro.

Ensuite, nous faisons appel à la fonction listen() pour se mettre à l’écoute de connexions à venir. En appelant listen(), nous convertissons notre socket en une socket passive qui peut accepter des connexions. Faire appel à listen() est assez simple :

if ( listen( listenfd, 5 ) < 0 ) {
   perror( "listen" );
   exit(1);
}

listen() prend deux arguments : le descripteur de fichier de la socket et le backlog.
L’argument backlog est utilisé pour limiter le nombre de connexions en attente d’une connexion.
Ainsi, en passant 5 nous indiquons au noyau de créer une file d’attente pour 5 connexions.
Si un client tente de se connecter alors que la file d’attente est pleine, le noyau refusera la connexion, et l’appel de la fonction connect() du client retournera une erreur de refus de connexion.
Remarquez que le backlog ne spécifie pas le nombre maximum de connexions que le serveur peut gérer, parce qu’une fois qu’une connexion est acceptée par le serveur, la requête est enlevée de la file d’attente, laissant place à une nouvelle requête de connexion.

Ensuite, nous appelons la fonction accept() à l’intérieur d’une boucle rudimentaire. Accept() connecte au serveur la requête de connexion en tête de la file d’attente, et retourne un descripteur de fichier de socket pour cette connexion. Nous pouvons alors lire et écrire des données au client en utilisant cette nouvelle socket. Voyons comment notre petit serveur enverra des messages à ces clients :

for (;;) {
   char *buffer = "Coucou !n";

   if ( (connectfd = accept( listenfd,
          (struct sockaddr *)NULL, NULL )) < 0 ) {
      perror( "accept" );
      exit(1);
   }

   if ( write( connectfd, buffer, strlen(buffer)) < 0 ) {
      perror( "write" );
      exit(1);
   }
   close( connectfd );
}

La boucle rudimentaire que j’ai mentionnée plus haut est construite autour d’une boucle infinie ; le serveur continuera d’attendre des requêtes de connexion jusqu’à ce que l’utilisateur tue le processus (en utilisant Ctrl-c par exemple).
Notre serveur n’est pas très flexible étant donné que le message qu’il envoie aux clients qui se connectent est codé en dur : “Coucou !”.
Le message est codée par une chaîne de caractères terminée par null.
Dans l’appel de la fonction accept(), nous passons le descripteur de fichier de la socket d’écoute (et rien pour la structure d’adresse puisque nous n’avons pas besoin de l’information qu’accept() retourne dans cette structure), et en retour, accept() nous donne le descripteur de fichier de la socket connectée.
Cette socket, connectfd, est notre bout de connexion entre la machine serveur et la machine client.
C’est sur cette socket que nous lisons et écrivons des données quand nous communiquons avec le client.

Enfin, après avoir envoyé le message, nous fermons la socket en utilisant la fonction close() et nous retournons au sommet de la boucle, prêts à accepter une autre connexion.

En mettant toutes les pièces du puzzle ensemble (avec la gestion d’erreur), nous obtenons le code suivant pour une petite application serveur :

#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main( int argc, char **argv )
{
   struct sockaddr_in serverAddress;
   int listenfd, connectfd;

   if ( (listenfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
      perror( "socket" );
      exit(1);
   }

   bzero( &serverAddress, sizeof(serverAddress) );
   serverAddress.sin_family = AF_INET;
   serverAddress.sin_port = htons(12345);
   serverAddress.sin_addr.s_addr = htonl( INADDR_ANY );

   if ( bind( listenfd, (struct sockaddr *)&serverAddress,
               sizeof(serverAddress) ) < 0 ) {
      perror( "bind" );
      exit(1);
   }

   if ( listen( listenfd, 5 ) < 0 ) {
      perror( "listen" );
      exit(1);
   }

   for (;;) {
      char *buffer = "Coucou !n";

      if ( (connectfd = accept( listenfd,
             (struct sockaddr *)NULL, NULL )) < 0 ) {
         perror( "accept" );
         exit(1);
      }

      if ( write( connectfd, buffer, strlen(buffer) ) < 0 ) {
         perror( "write" );
         exit(1);
      }

      close( connectfd );
   }
}

Et ça, mes amis, c’est notre serveur. Si vous avez créé un projet de type outil/utilitaire standard pour le client et le serveur, vous pouvez les compiler et les exécuter chacun. Cependant, avant de démarrer le client, assurez vous que le serveur tourne déjà. Si vous voulez compiler et exécuter depuis un shell, tapez les commandes suivantes pour invoquer le compilateur :

% cc -o server server.c

% cc -o client client.c

Une fois les codes compilés, vous pouvez ouvrir un second shell pour en avoir un pour le client et un pour le serveur.
Encore une fois, assurez vous d’avoir d’abord lancé le serveur (en tapant ./server dans le répertoire où vous avez compilé le code) avant de lancer le client.
Si vous voulez voir quelque chose de sympa, tapez la commande suivante dans le shell pendant que le serveur tourne (c’est également une bonne façon de tester les applications serveurs).

% telnet localhost 12345

Voici donc un exemple simple qui vous montre comment fonctionne le réseau sous Unix. Si vous êtes un peu intéressé par le réseau, je vous recommande chaudement le livre de W. Richard Stevens : Unix Network Programming, Volume 1. Avec l’importance du réseau dans la plupart des applications d’aujourd’hui, tous les programmeurs devraient avoir ce livre. Il y a beaucoup plus à dire sur le réseau que ce que nous avons vu aujourd’hui. Il y a plein de choses à prendre en considération pour la montée en charge, l’indépendance vis-à-vis des protocoles, la sécurité et plein d’autres. Ce livre couvre toutes ces questions.

Dans le prochain article, nous utiliserons ce que nous avons appris aujourd’hui et nous verrons comment utiliser les classes Foundation de Cocoa pour faire de RCE une application qui communique par le réseau.

Textes originaux en anglais sur O’Reilly : Networking and the BSD Sockets API par Mike Beam

opoppon Programmation Cocoa , , , ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
S'abonner aux commentaires de cet article