Les sockets
Les sockets constituent un mécanisme de communication entre processus sur un même ordinateur ou sur deux systèmes reliés par un réseau.
En pratique, un socket est une extrémité d'un canal de communication. Il faut donc deux sockets pour communiquer, chacun à une extrémité du canal. Le canal de communication est géré par le système d'exploitation, c'est lui qui gèrera la façon dont sont transmise les données, dans les applications il suffira d'écrire les données sur le socket.
Notez que la plupart des livres parlent de socket au féminin, et une socket se prononcerait "une soquète", mais je les emmerdes.
Communication entre socket
Pour créer le canal de communication, les sockets ont deux propriétés que nous appellerons le domaine et le nom.
Le domaine spécifie le type de communication attendue pour le socket, c'est à dire le type de canal que le système d'exploitation doit mettre en place. Ce document présentera la communication réseau (INET) et locale (LOCAL), chacune pouvant être en datagramme (DGRAM) ou en flux (STREAM). La connexion entre deux sockets se fait au sein d'un même domaine, il est impossible de connecter un socket local et un socket réseau, ou même connecter ceux en mode datagramme avec ceux en flux. Pas d'inquiétude si tout n'est pas clair ici, tout sera présenté ultérieurement.
La seconde propriété est le nom du socket. Lors d'une communication entre deux socket, un des deux attends une connexion, et l'autre s'y connecte, le canal est alors bidirectionnel. Pour attendre une connexion, un socket demande au système d'exploitation (grâce à l'appel bind()) à être disponible pour un "nom" particulier, ce sera l'adresse à laquelle ce socket sera disponible. Dans le cas d'une communication réseau le "nom" est l'adresse IP et le port sur lequel le socket est disponible, pour une communication locale c'est un fichier du système d'exploitation qui est utilisé comme adresse.
Un second socket se connecte au socket en attente en précisant son adresse. Le second socket n'utilise pas bind() mais une des méthodes de connexion. Sa propre adresse (IP et port pour le réseau) sera déterminé par le système d'exploitation.
Propriétés
Pour le réseau, les sockets fournissent le moyen de créer de d'utiliser des connections TCP ou UDP sur IP comme nous le verrons. Ces connections permettent à deux processus d'échanger des informations ce qui en fait un moyen de communication interprocessus. Les données sont transmise à l'aide du socket, et c'est le noyau qui fait l'encapsulation des technologies réseaux sous-jacente.
Pour la communication locale, les sockets fournissent un moyen simple d'obtenir une communication bidirectionnelle entre des processus. Les processus peuvent utiliser des technologie très différentes (Java, Mono, Python, etc) et communiquer à l'aide de sockets. Un intérêt de la communication locale à l'aide de sockets est qu'il est facile de la transformer en communication réseau lors de l'évolution du logiciel.
Un avantage de la communication interprocessus à l'aide des sockets est la totale indépendances des systèmes d'exploitation et des langages de programmation. On peut créer un serveur en langage C qui ouvre un socket sur un ordinateur Linux, et un client en Python sous openBSD.
Mise en place d'un socket
Création d'un socket
La fonction socket() permet de créer un socket grâce auquel on pourra communiquer avec d'autres processus. Sa signature est la suivante:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
Cette fonction crée un nouveau socket, elle renvoie un descripteur sur celui-ci ou -1 en cas d'échec. En cas d'échec, la valeur globale errno sera fixé afin d'en donner la cause.
Les trois arguments donnent le type de socket, et donc le type de communication entre processus.
Le domaine spécifie le type de connexions entre les processus, les différents valeur possible sont données dans le fichier d'en tête sys/type.h, les plus importantes sont PF_INET (pour IPv4), PF_INET6 (pour IPv6) et PF_LOCAL (pour une communication locale identifiée par un fichier).
Notez qu'on parle de socket Unix pour un socket PF_LOCAL et on retrouve régulièrement du code utilisant PF_UNIX au lieu de PF_LOCAL, il s'agit de la même chose.
Notez que les adresses utilisent des constantes pour lesquels le PF du socket est remplacé par AF (par exemple AF_INET dans IPv4). Pourquoi cette différence?, c'est une longue histoire et j'ai pas le temps, mais pensez à utiliser PF pour les sockets et AF pour les adresses.
L'argument "type" permet de donner le type de communication assurée entre les deux processus. Les types que nous verrons sont SOCK_STREAM pour une communication sûre avec un flux de données sans duplication et SOCK_DGRAM pour une communication non sûre. Ces deux types de communications sont vue dans la suite.
Protocole permet de préciser le protocole de communication. Dans la plupart de cas, le protocole peut être déduit du domaine et du type, on peut alors préciser 0 comment protocole.
Exemple d'utilisation de socket:
int sock; sock = socket(PF_LOCAL, SOCK_DGRAM, 0);
Ceci crée un socket Unix de type datagramme.
Donner une adresse à un socket
Une fois un descripteur obtenu, on peut envoyer des données à une autre socket ou se mettre en attente de données provenant d'une autre socket. Seules des sockets de même domaine, type et protocole peuvent communiquer entre elles.
Pour qu'une socket puisse envoyer des données vers une autre, elle doit être capable de la désigner. L'appel système permettant d'attribuer une identification (on dit une adresse) à une socket pour que d'autres puissent entrer en communication avec elle est
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
sockfd est le descripteur de la socket, addr est l'adresse en mémoire d'une structure contenant l'adresse que l'on veut attribuer à la socket et addrlen est la longueur en octets de cette stucture.
Le format de l'adresse et donc la structure contenant l'adresse varie selon le domaine de la socket.
Pour les sockets du domaine AF_LOCAL, l'adresse est le nom d'un fichier tandis que pour le domaine AF_INET, il s'agit d'une adresse IP.
La fonction bind() retourne 0 si elle réussit et -1 en cas d'échec.
Pour le domaine AF_LOCAL, il faut utiliser une structure struct stockaddr_un définie dans <sys/un.h>. Cette structure contient les champs:
- sun_family qui doit prendre la valeur AF_LOCAL
- sun_path qui sous linux est un tableau de 108 caractères
qui doit contenir le nom d'un fichier. Le fichier indiqué par sun_path ne doit pas exister et sera créé par la fonction bind(). La commande ls -l,
affichera comme type pour ce fichier la lettre s (pour socket).
Pour le domaine AF_INET, il faut utiliser une structure sockaddr_in définie dans le fichier <netinet/in.h> et qui contient les champs
- sin_family qui doit prendre la valeur AF_INET
- sin_port (unsigned short) qui est le n° de port
- sin_addr (struct in_addr) qui permet de spécifierl'adresse IP.
La structure in_addr contient le champ
- s_addr (unsigned long int) qui contient l'adresse IP sous la forme des 4 octets qui la composent.
L'ordre des bits pour représenter une valeur numérique entière peut varier d'un système à un autre. Certains systèmes stockent les bits dans l'ordre du plus significatif au moins significatif, on appelle cette représentation "big endian", d'autres dans l'ordre inverse, c'est la représentation "little indian" (c'est le cas sur l'architecture PC). Il faut donc prendre des précautions quand on envoie des données entières sur le réseau car l'émetteur et le destinataire pourraient utiliser des représentations différentes.
En particulier, le numéro de port et l'adresse IP d'une socket seront transmis à celles avec lesquelles elle communiquera. Il faut donc fixer une convention pour la représentation de ces valeurs lors des communications sur le réseau. La représentation retenue sur Internet est la big endian.
Afin de rendre les applications portables, la bibliothèque C fournit des fonctions pour convertir des valeurs entières (unsigned short pour le port et unsigned long pour l'IP) dans leur représentation sur le réseau. Ce sont les fonctions
#include <netinet/in.h> uint32_t htonl (uint32_t hostlong); uint16_t htons (uint16_t hostshort); uint32_t ntohl (uint32_t netlong); uint16_t ntohs (uint16_t netshort);
Les fonctions hton effectuent la conversion de la représentation du système (h pour host) vers celle du réseau (n pour network) et les fonction ntoh les conversions inverses. La lettre l est pour les entiers longs et la lettre s pour les entiers courts.
On utilisera systématiquement ces fonctions même si sur les systèmes qui utilisent la représentation big endian elles ne font que retourner la valeur de leur paramètre.
L'indication d'adresses IP sous forme d'entiers n'est pas très pratique. La bibliothèque C fournit une fonction qui permet de convertir une chaîne de caractères contenant une adresse IP dans la notation pointée à 4 nombres décimaux en une valeur entière dans la représentation du réseau. Il s'agit de lafonction
#include <arpa/inet.h> in_addr_t inet_addr (const char *cp);
Il existe également une fonction qui remplit la structure struct in_addr à partir d'une chaîne de caractères et une fonction qui effectue l'opération inverse:
#include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, structin_addr *inp); char * inet_ntoa (struct in_addr in);
Il existe également une fonction qui permet d'obtenir l'adresse IP à partir d'un nom de machine. Il s'agit de la fonction
#include <netdb.h> struct hostent *gethostbyname(const char *name);
La structure hostent pointée par le pointeur retourné par la fonction contient entre autres le membre char **h_addr_list; qui est un tableau de chaînes de caractères terminé par un pointeur NULL contenant les adresses IP de l'hôte dont le nom est donné dans le paramètre name.
Les applications utilisant des sockets ont souvent une architecture client/serveur dans laquelle un processus serveur utilise une socket pour recevoir les requêtes des processus clients et leur renvoie des réponses.
Dans le domaine AF_INET, si un serveur dispose de plusieurs adresses IP, en utilisant la valeur INADDR_ANY définie dans <netinet/in.h> pour le membre s_addr de la structure in_addr, le serveur recevra les requêtes destinées à n'importe laquelle de ses adresses IP sur le port de la socket.
Du côté du client, il n'est pas nécessaire d'utiliser bind() car l'adresse IP et un n° de port libre seront automatiquement attribués lors d'une requête.
L'utilisation de n° de port inférieurs à 1024 est réservée aux processus tournant au nom de l'administrateur.
Se connecter à un socket
Différents selon la communication avec datagramme ou avec flux, voir plus loin.
Communication avec des datagrammes
La communication entre processus à l'aide de datagramme permet d'envoyer des données de façon bidirectionnelle entre deux socket de façon non sûr et non séquencé. Une communication non sûre signifie que
Si on utilise le réseau (par exemple PF_INET), une communication avec SOCK_DGRAM correspond à une communication UDP.
Envoyer des datagrammes
L'envoi de messages pour les sockets de type SOCK_DGRAM dans le domaine AF_LOCAL ou AF_INET s'effectue en utilisant la fonction
#include <sys/types.h> #include <sys/socket.h> int sendto(int sock, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
où
- sock est le descripteur la socket
- msg est l'adresse des données à envoyer
- len est le nombre d'octets des données
- flags est 0 ou une combinaison à l'aide de ou (|) de constantes qui permettent de spécifier des options de la fonction sendto(), citons la constante MSG_DONTWAIT qui permet un appel non bloquant (un appel bloquant peut survenir s'il n'y a plus suffisamment de place dans le tampon du noyau attribué à la socket pour stocker les données)
- to est l'adresse de la socket destinataire du message
- lento est la longueur de to
La fonction renvoie le nombre d'octets émis ou -1 en cas d'échec.
Recevoir les datagrammes
La réception de messages pour les sockets de type SOCK_DGRAMdans le domaine AF_UNIX ou AF_INET s'effectue en utilisant la fonction
#include <sys/types.h> #include <sys/socket.h> int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, socklen_t *fromlen);
Les paramètres ont la même signification que dans sendto si ce n'est que les constantes pour flags sont différentes et que from et fromlen (qui doit pointer vers un entier contenant la taille de la zone from à l'appel de la fonction) recoivent l'adresse de l'émetteur et sa longueur. On peut passer des pointeurs NULL si on ne souhaite pas récupérer ces informations.
La valeur de retour est le nombre d'octets reçus ou -1 en cas d'erreur.
Exemple avec un socket unix
Serveur
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/un.h>
#include <string.h>
#include <stdlib.h>
int main()
{
unlink("socket1");
int sfd;
sfd = socket(PF_LOCAL, SOCK_DGRAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "socket1");
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
sleep(2);
char msg[100];
recvfrom(sfd, msg, 100, 0, NULL, NULL);
printf("Premier message: %s\n", msg);
recvfrom(sfd, msg, 100, 0, NULL, NULL);
printf("Second message: %s\n", msg);
return EXIT_SUCCESS;
}
Client
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/un.h>
#include <string.h>
#include <stdlib.h>
int main()
{
sleep(1);
int sfd;
sfd=socket(PF_LOCAL, SOCK_DGRAM, 0);
struct sockaddr_un to;
to.sun_family = AF_LOCAL;
strcpy(to.sun_path, "socket1");
char *msg1 = "Message 1";
sendto(sfd, msg1,strlen(msg1) + 1, 0, (struct sockaddr *)&to, sizeof(to));
char *msg2 = "Message 2";
sendto(sfd, msg2,strlen(msg2) + 1, 0, (struct sockaddr *)&to, sizeof(to));
return EXIT_SUCCESS;
}
Si on lance simultanément les 2 programmes (ou le programme 2 après le programme 1) on obtient pour le programme 1 l'affichage
Premier message: Message 1 Second message: Message 2
L'instruction sleep(1) dans le programme 2 sert à assurer que la socket du programme 1 aura été créée avant l'appel à sendto() en cas de lancement simultané. L'instruction sleep(2) dans le programme 1 sert à assurer que les deux messages seront envoyés avant l'appel à
recvfrom() lors d'un lancement simultané pour illustrer le fait que les deux appels à recvfrom correspondent bien aux deux appels à sendto().
On remarque également qu'il n'y a pas eu de bind() dans le programme 2. Il y a donc eu création d'une socket "anonyme".
Exemple avec UDP et IP
Serveur
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main()
{
int sfd;
sfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(5555);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
struct sockaddr_in from;
socklen_t fromlen;
char msg[100];
char *answer = "Message reçu";
while ( 1 ) {
recvfrom(sfd, msg, 100, 0, (struct sockaddr *)&from, &fromlen);
printf("Message de %s:%d: '%s'\n",
inet_ntoa(from.sin_addr),
ntohs(from.sin_port),
msg);
sendto(sfd,
answer,
sizeof(answer),
0,
(struct sockaddr *)&from,
sizeof(from));
}
return EXIT_SUCCESS;
}
Client
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main()
{
int sfd;
sfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in to;
to.sin_family = AF_INET;
to.sin_port = htons(5555);
to.sin_addr.s_addr = inet_addr("127.0.0.1");
char *msg = "Bonjour";
char answer[100];
struct sockaddr_in from;
socklen_t fromlen;
sendto(sfd,
msg, strlen(msg) + 1, 0,
(struct sockaddr *)&to, sizeof(to));
recvfrom(sfd, answer, 100, 0, (struct sockaddr *)&from, &fromlen);
printf("Réponse de %s:%d: '%s'\n", inet_ntoa(from.sin_addr),
ntohs(from.sin_port), answer);
return EXIT_SUCCESS;
}
Résultats:
côté serveur:
Message de 10.0.0.2:3333: 'Bonjour' Message de 10.0.0.3:1026: 'Bonjour'
.côté client (prog 2):
Réponse de 10.0.0.3:5555: 'Message reçu'
Communication à l'aide de flux
Pour les sockets du type SOCK_STREAM, on utilise un mode connecté dans lequel il n'est pas nécessaire de préciser à chaque envoi l'adresse du destinataire, celle-ci étant fixée pour toute la connexion. De plus, les choses se passent de manière fort différente du côté serveur et du côté client.
Côté serveur, on utilise une socket qui se met à l'écoute des connexions. Lorsqu'une demande de connexion survient, on peut l'accepter ce qui engendre la création d'une nouvelle socket avec laquelle sera réalisée la suite du dialogue laissant la socket initiale libre pour recevoir de nouvelles connexions.
Côté client, la socket doit effectuer une demande de connexion au socket à l'écoute sur le serveur puis une fois la demande acceptée, elle peut entamer le dialogue avec la socket créée lors de l'acceptation de la connexion sur le serveur.
Écouter les connexions
Du côté serveur, la fonction qui permet de mettre une socket du type SOCK_STREAM à l'écoute de connexion est la fonction
#include <sys/socket.h> int listen(int sock, int backlog);
où sock est le descripteur de la socket et backlog un entier indiquant le nombre maximum de connexions non encore acceptées que la socket pourra mémoriser.
Si une demande de connexion survient alors que le nombre de connexions établies non encore acceptées vaut backlog, la connexion n'est pas établie et la fonction demandant la connexion côté client renvoie un code d'erreur. Sinon la connexion est établie et mise dans un état "pendant" (dans une file d'attente d'acceptation).
Établir une connexion
Pour accepter une connexion en attente, on effectue un appel à la fonction
#include <sys/types.h> #include <sys/socket.h> int accept(int sock, struct sockaddr *adresse, socklen_t *longueur);
où sock est le descripteur de la socket sur laquelle a été précédemment réalisé un appel à listen, adresse est l'adresse d'une zone ou sera écrite l'adresse de la socket client à l'autre extrémité de la connexion et longueur l'adresse d'un entier qui doit contenirà l'appel de la fonction la taille de la zone "adresse" et qui contiendra au retour le nombre d'octets écrits dans cette zone. On peut passer des pointeurs NULL si on ne souhaite pas récupérer ces informations.
En cas d'échec, la fonction retourne -1. Sinon elle retourne le descripteur d'une socket sur laquelle on pourra lire les requêtes envoyées par le client et écrire les réponses.
La communication ne s'effectue donc jamais via la socket d'écoute qui reste libre pour établir et accepter d'autres connexions.
Le dialogue entre la socket du client et la "socket de service" du serveur sera d'ailleurs souvent réalisé par un processus fils ou un thread afin de
permettre au serveur d'accepter rapidement de nouvelles connexions. L'appel est bloquant s'il n'y a pas de connnexion en attente.
Se connecter au serveur coté client
Côté client, la connexion à une socket serveur s'effectue par la fonction
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
où sockfd est la socket cliente, serv_addr l'adresse de la socket serveur et addrlen la longueur en octet de serv_addr.
L'écriture ou la lecture sur une socket cliente ou de service peut s'effectuer par les appels système write et read.
La lecture est bloquante s'il n'y a pas de données disponibles sur la socket. L'écriture peut être bloquante si le tampon du noyau contenant les données à envoyer est plein. read() retourne 0 quand on a lu toutes les données d'une socket et que celle-ci a été fermée à l'autre extrémité.
On peut fermer une socket en utilisant close, comme on fermerais un descripteur de fichier.
Exemple
Serveur
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main()
{
int sfd;
sfd=socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family= AF_INET;
addr.sin_port =htons(5555);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr*)&addr,sizeof(addr));
listen(sfd, 10);
char *answer = "Message reçu";
while( 1 ){
printf("Attente ...\n");
int sock_service;
struct sockaddr_in from;
socklen_t fromlen;
sock_service = accept(sfd, (struct sockaddr*)&from, &fromlen);
printf("Connexion avec %s:%d\n", inet_ntoa(from.sin_addr),
ntohs(from.sin_port));
int msg_len;
char msg[100];
msg_len =read(sock_service, msg, 100);
printf("Message: ");
fflush(stdout);
write(STDOUT_FILENO,msg, msg_len);
printf("\n");
write(sock_service,answer,sizeof(answer));
close(sock_service);
}
return EXIT_SUCCESS;
}
Client
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int sfd;
sfd=socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in to;
to.sin_family= AF_INET;
to.sin_port =htons(5555);
to.sin_addr.s_addr=inet_addr("127.0.0.1");
connect(sfd, (struct sockaddr*)&to,sizeof(to));
char *msg= "Bonjour";
write(sfd,msg,strlen(msg));
printf("Message envoyé\n");
char answer[100];
int answer_len;
answer_len=read(sfd,answer, 100);
printf("Réponse: ");
fflush(stdout);
write(STDOUT_FILENO,answer,answer_len);
printf("\n");
close(sfd);
return EXIT_SUCCESS;
}
Résultat
Côté client
Message envoyé Réponse: Message reçu
Côté serveur
Attente ... Connexion avec 10.0.0.2:3333 Message: Bonjour Attente ... Connexion avec 10.0.0.3:1026 Message: Bonjour Attente ... Connexion avec 10.0.0.3:1027 Message: Bonjour Attente ...
En pratique, le dialogue entre la socket de service et celle du client s'effectue dans un autre processus ou thread pour permettre au processus serveur d'accepter rapidement de nouvelles connexions afin d'assurer un temps de réponse raisonnable et de ne pas remplir la file de connexions en attente et avoir à refuser des clients.
Les codes suivants illustrent ces deux possibilités.
Serveur
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <signal.h>
#include <arpa/inet.h>
int main()
{
int sfd;
sfd=socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family= AF_INET;
addr.sin_port =htons(5555);
addr.sin_addr.s_addr= INADDR_ANY;
bind(sfd, (struct sockaddr*)&addr,sizeof(addr));
listen(sfd, 10);
while( 1 )
{
int sock_service;
struct sockaddr_in from;
socklen_t fromlen;
sock_service =accept(sfd, (struct sockaddr*)&from, &fromlen);
switch(fork()) {
case -1:
perror("Erreur lors dufork");
break;
case 0:
{
printf("Connexion avec %s:%d\n",
inet_ntoa(from.sin_addr),
ntohs(from.sin_port));
close(sfd);
dup2(sock_service, STDIN_FILENO);
dup2(sock_service, STDOUT_FILENO);
close(sock_service);
execl("service", "service", NULL);
perror("Erreur lors duexecl");
}
default:
close(sock_service);
}
}
}
Programme de service (à compiler sous le nom "service")
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
char msg[100];
read(STDIN_FILENO, msg, 100);
char *answer = "Message reçu";
write(STDOUT_FILENO, answer, sizeof(answer));
return EXIT_SUCCESS;
}
Remarque
On ne peut plus afficher le messsage lu sur la sortie standard car celle-ci est redirigée sur la socket de service.
Il s'agit ici de la forme générale d'un serveur TCP.
On aurait pu placer le code du dialogue directement dans le code du serveur pour éviter l'appel à exec mais la méthode consistant à utiliser un autre programme pour réaliser le dialogue avec le client est plus générale. Elle permet entre autres d'utiliser des programmes de service différents
selon le client et facilite le débogage. Ceci est d'autant plus vrai si on redirige les entrée/sortie standards du processus de service vers la socket, ce qui permet de tester le programme de dialogue indépendamment.
Néanmoins les solutions basé sur les threads sont généralement beaucoup plus rapide, comme le montre l'exemple suivant:
Serveur
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdlib.h>
#include <arpa/inet.h>
void *service(void *arg)
{
int sock_service = (int)arg;
struct sockaddr_in from;
unsigned int from_len=sizeof(from);
getpeername(sock_service, (struct sockaddr*)&from, &from_len);
printf("Connexion avec %s:%d\n",inet_ntoa(from.sin_addr),
ntohs(from.sin_port));
char msg[100];
int msg_len;
msg_len= read(sock_service,msg, 100);
printf("Message: ");
fflush(stdout);
write(STDOUT_FILENO, msg,msg_len);
printf("\n");
char *answer = "Message reçu";
write(sock_service,answer, sizeof(answer));
close(sock_service);
return NULL;
}
int main()
{
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
int sfd;
sfd=socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family= AF_INET;
addr.sin_port =htons(5555);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr*)&addr,sizeof(addr));
listen(sfd, 10);
while( 1 ) {
int sock_service;
pthread_t t;
sock_service = accept(sfd, NULL, NULL);
if (pthread_create(&t, &attr, service, (void*)sock_service) == -1 ) {
perror("Erreur pthread_create");
close(sock_service);
}
}
return EXIT_SUCCESS;
}
A noter l'utilisation de la fonction
#include<sys/socket.h> int getpeername(int sock, struct sockaddr *name, socklen_t *namelen);
qui permet de récupérer l'adresse de la socket à l'autre extrémité de la connexion de la socket de descripteur sock.
Il existe également une fonction qui permet de récupérer l'adresse d'une socket à partir de son descripteur. Il s'agit de la fonction
#include<sys/socket.h> int getsockname(ints,struct sockaddr*name, socklen_t *namelen);
Ressources
L'article sur les sockets de Wikipedia FR.
L'article anglais de wikipedia est bien mieux fait.
Un guide sur les socket et sa traduction française.

