Bien programmer en langage C
Date de publication : 28 mai 2008
VII. Notes brutes concernant les réseaux
VII-A. Ouvrages de références
VII-B. Dialogue Terminal Modem
VII-C. Réalisation d'un projet réseau
VII-C-1. Sockets
VII-C-1-a. Introduction
VII-C-1-b. Protocoles
VII-C-1-c. Fonctions
VII-C-1-d. Serveur
VII-C-1-e. Client
VII-C-2. Réalisation d'un Client / Serveur UDP/IP
VII-C-2-a. Serveur UDP/IP
VII-C-2-b. Client UDP/IP
VII-C-3. Réalisation d'un Client / Serveur TCP/IP
VII-C-3-a. Serveur TCP/IP
VII-C-3-b. Client TCP/IP
VII-C-3-c. Serveur multiclient en TCP/IP
VII-D. Réalisation pas à pas d'un serveur TCP/IP
VII-D-1. Introduction
VII-D-1-a. Avertissement
VII-D-1-b. Environnement de développement
VII-D-1-c. Environnement de test
VII-D-2. 01 - Initialisation, terminaison
VII-D-3. 02 - Création, suppression de sockets
VII-D-4. 03 - Mini serveur : Mise en écoute du port 23
VII-D-5. 04 - Mini serveur : Connexion du client/ Déconnexion du client
VII-D-6. 05 - Reception d'un caractère puis fermeture
VII-D-7. 06 - Mini serveur : Reception d'un bloc de données puis fermeture sur ESC
VII-D-7-a. Pseudo-code
VII-D-7-b. Code
VII-D-8. 07 - Mini serveur : Reception d'un bloc de données puis deconnexion sur ESC
VII-D-8-a. Pseudo-code
VII-D-8-b. Code
VII-D-9. 08 - Serveur-multi-clients : Reception d'un bloc de données puis deconnexion sur ESC
VII-D-9-a. Pseudo-code
VII-D-9-b. Code
VII-D-10. Conclusion
VII-E. Transmettre du texte par sockets
VII-E-1. Emission de petits blocs (cas simple)
VII-E-2. Reception de petits blocs
VII-E-3. Emission de gros blocs (cas général)
VII-E-4. Réception de gros blocs
VII-F. Etude de cas : transmettre un fichier texte.
VII-F-1. Spécifications
VII-F-2. Conception
VII-F-2-a. Protocole
VII-F-2-a-i. Format des messages
VII-F-2-a-ii. Commandes
VII-F-2-a-iii. Réponses
VII-F-2-a-iv. Exemple de dialogue
VII-G. Du bon usage de select()
VII-G-1. Introduction
VII-G-2. Programmation
VII-G-3. Exemples d'utilisation
VII-G-3-a. Suspension temporisée
VII-G-3-b. Surveillance d'un flux en réception
VII-G-3-c. Surveillance de deux flux en réception
VII-G-4. Conclusion
VII-H. La récupération de l'IP à partir du nom du serveur
VII-I. Ebauche d'une bibliothèque sockets portable (psock)
VII-I-1. Interfaces communes
VII-I-2. Sockets UNix (sun)
VII-I-3. Sockets IP (Internet Protocol)
VII-J. Ressources
VII. Notes brutes concernant les réseaux
VII-A. Ouvrages de références
tome 1 du 'Unix network programming' de W. Richard Stevens
VII-B. Dialogue Terminal Modem
Terminal Modem
| |
| commande |
|------------------->|
| réponse | Traitement
|<------------------ |
Traitement | |
|
VII-C. Réalisation d'un projet réseau
La realisation d'un projet réseau est basée sur l'utilisation des sockets (BSD, Windows). Il y a quelques
nuances selon le système utilisé, mais dans l'ensemble, les fonctions et comportements sont identiques.
Le but de cet article est de montrer la réalisation de clients / serveurs en UDP/IP et TCP/IP.
VII-C-1. Sockets
VII-C-1-a. Introduction
Les sockets sont des flux de données (octets) très similaires aux flux d'entrée / sortie standards ou
aux fichiers, mais qui permettent de réaliser des connexions de données bidirectionnelles entre
des machines (locales ou distantes) via un réseau de données (boucle, réseau local, Internet,
X.25 etc.). Ils sont mis en oeuvre via une série de fonctions regroupées sous le nom de API sockets
ou sockets BSD ou sockets.
VII-C-1-b. Protocoles
Les connexions de données sont gérées par différent protocoles (niv 3 et 4) et différentes liaisons (niv.
2) et interfaces physiques (niv. 1). Le sockets ignorent les interfaces (gérés par les drivers
systèmes), mais connaissent les protocoles de niveau 3 et 4 (IP, UNIX / TCP, UDP, etc.). Ensuite,
ils savent travailler en mode non connecté (datagrams, simple, pas de vérification) ou connecté
(paquets, plus complexe, données vérifiée, integrité des données).
Par exemple, le protocole de niveau 3 IP (Internet Protocol) sait travailler en mode de niveau 4 connecté
(TCP) ou non connecté (UDP).
VII-C-1-c. Fonctions
Les sockets sont manipulés par des fonctions générales :
-
socket()
-
close() ou closesocket()
-
bind()
-
send(), recv(), (mode connecté)
-
sendto(), recvfrom() (mode non connecté)
-
et des fonctions spécialisées qui dépendent si l'application est un serveur ou un client et si on utilise
un mode connecté ou non.
VII-C-1-d. Serveur
-
listen()
-
accept() (mode connecté)
VII-C-1-e. Client
-
connect() (mode connecté)
Rappel : La documentation complète des fonctions se trouve dans les 'pages man', comme, par exemple,
celles-ci.
VII-C-2. Réalisation d'un Client / Serveur UDP/IP
Client Serveur
| |
| indication |
|<- - - - - - - - - |
| commande |
|------------------->|
| [réponse] | Traitement
|<- - - - - - - - - |
[Traitement]| |
|
Le mode UDP (Datagram sockets) est rustique et ne necessite pas de connexion. Il agit par échange de
blocs de données appelés 'Datagrammes'.
VII-C-2-a. Serveur UDP/IP
Les opérations à réaliser sont:
-
initialisation
-
ouverture d'un socket en mode datagramme (UDP/IP): socket(AF_INET, SOCK_DGRAM, 0)
-
configurer l'adresse et le port: bind()
-
dans une boucle
-
tester la réception : select() [Facultatif]
-
lire le bloc reçu : recvfrom()
-
traitement des données...
-
émission du bloc : sendto()
-
fin
-
fermeture du socket : closesocket()
VII-C-2-b. Client UDP/IP
Les opérations à réaliser sont:
-
initialisation
-
ouverture d'un socket en mode datagramme (UDP/IP): socket(AF_INET, SOCK_DGRAM, 0)
-
selon la demande
-
fabrication des données à émettre...
-
émission du bloc : sendto()
-
tester la réception : select() [Facultatif]
-
lire le bloc reçu : recvfrom()
-
traitement des données reçues...
-
fin
-
fermeture du socket : closesocket()
VII-C-3. Réalisation d'un Client / Serveur TCP/IP
Client Serveur
| |
| dem. de connexion |
|------------------->|
| acceptation |
|<- - - - - - - - - |
[Traitement]| indication |
|<- - - - - - - - - |
| commande |
|------------------->|
| [réponse] | Traitement
|<- - - - - - - - - |
[Traitement]| |
| deconnexion |
|<- - - - - - - - - >|
|
Le mode TCP (Stream sockets) est solide. Il garanti la transmission des données. Il nécessite
l'établissement d'une connexion. Ensuite, les blocs de données peuvent être échangés.
VII-C-3-a. Serveur TCP/IP
Les opérations à réaliser sont:
-
initialisation
-
ouverture d'un socket en mode flux (TCP/IP): socket(AF_INET, SOCK_STREAM, 0)
-
configurer l'adresse et le port: bind()
-
configurer le nombre d'écoutes: listen()
-
dans une boucle
-
accepter une connexion: accept()
-
tester la réception : select() [Facultatif]
-
recevoir des données: recv()
-
émettre des données: send()
-
déconnecter : shutdown()
-
fin
-
fermeture du socket : closesocket()
VII-C-3-b. Client TCP/IP
Les opérations à réaliser sont:
-
initialisation
-
ouverture d'un socket en mode flux (TCP/IP): socket(AF_INET, SOCK_STREAM, 0)
-
connexion: connect()
-
selon la demande
-
émettre des données: send()
-
tester la réception : select() [Facultatif]
-
recevoir des données: recv()
-
fin
-
déconnecter : shutdown()
-
fermeture du socket : closesocket()
VII-C-3-c. Serveur multiclient en TCP/IP
Le principe est de réduire la boucle principale à la surveillance des connexions entrantes (accept())
et de créer un thread comprenant la boucle de traitement avec les données de la connexion client.
thread 'serveur' : dans une boucle :
-
attendre une connexion: accept()
-
créer un thread avec le socket client
thread 'client' : dans une boucle :
-
attendre des données et les lire: recv()
-
émettre des données en réponse: send()
Apres la boucle (deconnexion, timeout...)
VII-D. Réalisation pas à pas d'un serveur TCP/IP
VII-D-1. Introduction
Il s'agit d'une application de mise en oeuvre écrite en langage C. Le détail des fonctions (paramètres,
comportement) doit être consulté dans les documents de références habituels (msdn, man etc.).
VII-D-1-a. Avertissement
Cette réalisation demande une bonne connaissance du langage C. Cependant, je m'efforce de n'utiliser
que des concepts simples du langage. Si je juge qu'un point est difficile, je fais une remarque
ou je renvoie à une explication détaillée. En cas de difficulté avérée, me contacter directement.
VII-D-1-b. Environnement de développement
Le code est écrit et validé sous Windows avec Dev-C++/Code::Blocks et la blibiothèque -lws2_32. Il compile
sous Linux, mais n'est pas validé pour le moment. (Si quelqu'un veut le faire, il est le bienvenu)
VII-D-1-c. Environnement de test
Il suffit d'une machine supportant le protocole TCP/IP, et munie d'une adresse IP (sinon, utiliser 127.0.0.1).
Si on dispose de 2 machines, on peut espionner les échanges de trames avec EtherReal.
VII-D-2. 01 - Initialisation, terminaison
Sous Windows, il est nécessaire d'indiquer au système que le processus courant veut utiliser les sockets.
Ce n'est pas nécessaire sous Linux. Sous Windows, winsock2 est compatible avec les sockets BSD.
Il est donc indispensable de vérifier qu'on a bien la bonne version de sockets sur la machine cible.
De plus, Windows exige de signaler la fin d'utilisation des sockets par le processus.
Voici notre premier programme 'réseau'. Pour le moment, il ne fait rien sous unixoïde, mais sous Windows,
il vérifie la version des sockets.
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Sortie attendue :
VII-D-3. 02 - Création, suppression de sockets
Ce programme ne fait rien de visible. Il se contente de créer un socket en mode 'Internet' (IP), puis
de le fermer proprement.
Nouvelles fonctions : socket(), closesocket()
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Exemple de sortie attendue
WIN : winsock2: OK
socket 44 is now opened in TCP/ IP mode
closing socket 44 ...
the socket is now closed
|
VII-D-4. 03 - Mini serveur : Mise en écoute du port 23
Le port 23 est le port Telnet par défaut. La encore, rien de spectaculaire, si ce n'est que si la machine
dispose d'un pare-feu (comme Zone Alarm, par exemple), celui-ci signale que le programme veut agir
en tant que serveur sur le port SMTP. C'est la conséquence de l'exécution de la fonction 'listen()'.
Si le pare-feu demande confirmation, l'exécution du programme est suspendue jusqu'à ce que la confirmation
soit validée. Ensuite, comme il n'y a pour le moment rien d'autre à faire, le programme se termine.
|
Attention, sous unixoïde, le port 23 n'est accessible que si on est en root.
|
Nouvelles fonctions : bind(), listen(), htons(), htonl().
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# define TELNET 23
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin =
{ 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (TELNET);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , TELNET);
if (sock_err ! = SOCKET_ERROR)
{
}
else
{
err = 1 ;
}
}
else
{
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Exemple de sortie attendue
WIN : winsock2: OK
socket 44 is now opened in TCP/ IP mode
listening on port 23 ...
closing socket 44 ...
the socket is now closed
|
VII-D-5. 04 - Mini serveur : Connexion du client/ Déconnexion du client
Le serveur va maintenant se bloquer en attente de connexion d'un client. On peut alors lancer un client
Telnet sur cette machine (ou une autre sur le même réseau) avec le port 23. On constate alors,
sur le client, que la connection est immédiatement perdue. Normal, car le serveur a renvoyé une
deconnexion immédiate du socket client.
Nouvelles fonctions : accept(), shutdown(), inet_ntoa().
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# define TELNET 23
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin =
{ 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (TELNET);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , TELNET);
if (sock_err ! = SOCKET_ERROR)
{
printf (" waiting for a client connection on port %d...\n " , TELNET);
{
SOCKADDR_IN csin =
{ 0 } ;
int recsize = (int ) sizeof csin;
SOCKET csock = accept (sock, (SOCKADDR * ) & csin, & recsize);
if (csock ! = INVALID_SOCKET)
{
printf (" client connected with socket %d from %s:%d\n "
,csock
,inet_ntoa (csin.sin_addr)
,htons (csin.sin_port));
shutdown (csock, 2 );
printf (" closing client socket %d...\n " , csock);
closesocket (csock), csock = INVALID_SOCKET;
}
else
{
perror (" socket.accept " );
err = 1 ;
}
}
}
else
{
perror (" socket.listen " );
err = 1 ;
}
}
else
{
perror (" socket.bind " );
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Exemple de sortie attendue
WIN : winsock2: OK
socket 44 is now opened in TCP/ IP mode
listening on port 23 ...
waiting for a client connection on port 23 ...
client connected with socket 48 from 192 .168 .0 .17 :1290
closing client socket 48 ...
closing socket 44 ...
the socket is now closed
|
VII-D-6. 05 - Reception d'un caractère puis fermeture
Une fois que le client Telnet est lance, le serveur se bloque sur l'attente d'un bloc de donné. Le client
telnet envoyant en temps réel toutes la frappes du clavier, un seul caractère suffit à débloquer
le serveur qui envoi alors une commande de deconnexion.
Nouvelles fonctions : recv().
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# define TELNET 23
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin =
{ 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (TELNET);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , TELNET);
if (sock_err ! = SOCKET_ERROR)
{
printf (" waiting for a client connection on port %d...\n " , TELNET);
{
SOCKADDR_IN csin =
{ 0 } ;
int recsize = (int ) sizeof csin;
SOCKET csock = accept (sock, (SOCKADDR * ) & csin, & recsize);
if (csock ! = INVALID_SOCKET)
{
printf (" client connected with socket %d from %s:%d\n "
,csock
,inet_ntoa (csin.sin_addr)
,htons (csin.sin_port));
{
unsigned char data[128 + 1 ];
sock_err = recv (csock, data, (int ) sizeof data - 1 , 0 );
if (sock_err ! = SOCKET_ERROR)
{
size_t nb_rec = (size_t) sock_err;
data[nb_rec] = 0 ;
printf (" %u byte%s received:\n%s\n "
,(unsigned ) nb_rec
,nb_rec > 1 ? " s " : " "
,data);
fflush (stdout);
}
else
{
perror (" socket.recv " );
err = 1 ;
}
}
shutdown (csock, 2 );
printf (" closing client socket %d...\n " , csock);
closesocket (csock), csock = INVALID_SOCKET;
}
else
{
perror (" socket.accept " );
err = 1 ;
}
}
}
else
{
perror (" socket.listen " );
err = 1 ;
}
}
else
{
perror (" socket.bind " );
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Exemples de sortie attendues
L'utilisateur tape 'a' sur la console du client:
WIN : winsock2: OK
socket 44 is now opened in TCP/ IP mode
listening on port 23 ...
waiting for a client connection on port 23 ...
client connected with socket 48 from 192 .168 .0 .17 :1299
1 byte received:
a
closing client socket 48 ...
closing socket 44 ...
the socket is now closed
|
VII-D-7. 06 - Mini serveur : Reception d'un bloc de données puis fermeture sur ESC
L'ajout d'une boucle sur receive() permet de recevoir plusieurs caractères. Si on reçoit ESC (27), la
boucle est rompue et une demande de deconnexion est émise vers le client. Le serveur s'arrête.
VII-D-7-a. Pseudo-code
BEGIN
sock := socket ()
bind (sock, ALL_IP, 23 )
listen (sock, 5 )
csock := accept (sock, csin)
REPEAT
recv (csock, data)
send (csock, " OK " )
UNTIL data[0 ] = ESC
closesocket (csock)
closesocket (sock)
END
|
Ce fonctionnement est simple, mais il n'accepte qu'une seule connexion. Dès qu'elle se termine, le serveur
s'arrête. Les fonctions accept() et recv() sont bloquantes.
VII-D-7-b. Code
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# define TELNET 23
# define ESC 27
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin =
{ 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (TELNET);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , TELNET);
if (sock_err ! = SOCKET_ERROR)
{
printf (" waiting for a client connection on port %d...\n " , TELNET);
{
SOCKADDR_IN csin =
{ 0 } ;
int recsize = (int ) sizeof csin;
SOCKET csock = accept (sock, (SOCKADDR * ) & csin, & recsize);
if (csock ! = INVALID_SOCKET)
{
printf (" client connected with socket %d from %s:%d\n "
,csock
,inet_ntoa (csin.sin_addr)
,htons (csin.sin_port));
{
int end = 0 ;
do
{
unsigned char data[128 + 1 ];
sock_err = recv (csock, data, (int ) sizeof data - 1 , 0 );
if (sock_err ! = SOCKET_ERROR)
{
size_t nb_rec = (size_t) sock_err;
data[nb_rec] = 0 ;
printf (" %u byte%s received:\n%s\n "
,(unsigned ) nb_rec
,nb_rec > 1 ? " s " : " "
,data);
fflush (stdout);
if (data[0 ] = = ESC)
{
end = 1 ;
}
}
else
{
perror (" socket.recv " );
err = 1 ;
end = 1 ;
}
}
while (! end);
}
shutdown (csock, 2 );
printf (" closing client socket %d...\n " , csock);
closesocket (csock), csock = INVALID_SOCKET;
}
else
{
perror (" socket.accept " );
err = 1 ;
}
}
}
else
{
perror (" socket.listen " );
err = 1 ;
}
}
else
{
perror (" socket.bind " );
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
VII-D-8. 07 - Mini serveur : Reception d'un bloc de données puis deconnexion sur ESC
L'ajout d'une boucle sur accept() permet de recevoir plusieurs demandes de connexions à la suite. Le
serveur ne s'arrête jamais.
VII-D-8-a. Pseudo-code
BEGIN
sock := socket ()
bind (sock, ALL_IP, 23 )
listen (sock, 5 )
DO
csock := accept (sock, csin)
closesocket (csock)
FOREVER
closesocket (sock)
END
|
Afin de simplifier l'organisaton du code, les boucles sont implémentées dans les fonctions clients()
et client().
FUNCTION client (csock:SOCKET)
BEGIN
REPEAT
recv (csock, data)
send (csock, " OK " )
UNTIL data[0 ] = ESC
closesocket (csock)
END
FUNCTION clients (sock:SOCKET)
BEGIN
DO
csock := accept (sock, csin)
client (csock)
FOREVER
END
BEGIN
sock := socket ()
bind (sock, ALL_IP, 23 )
listen (sock, 5 )
clients (sock)
closesocket (sock)
END
|
Ce fonctionnement reste simple, mais il n'accepte qu'une seule connexion à la fois. Par contre, dès qu'elle
se termine, le serveur est près à accepter une nouvelle connexion. Les fonctions accept() et recv()
sont bloquantes. Si une demande de connexion se fait pendant que le serveur traite un client,
la connexion est établie, mais le serveur ne traite pas les données tant que la connexion courante
n'est pas terminée. listen(..., 5) permet de traiter jusqu'à 5 demandes simultanées.
VII-D-8-b. Code
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# define TELNET 23
# define PORT TELNET
# define ESC 27
struct cli
{
SOCKADDR_IN sin;
int recsize;
SOCKET sock;
int err;
} ;
static void * client (void * p_data)
{
struct cli * p_cli = p_data;
if (p_cli ! = NULL )
{
int end = 0 ;
do
{
unsigned char data[128 ];
int sock_err = recv (p_cli- > sock, data, (sizeof data - 1 ), 0 );
if (sock_err ! = SOCKET_ERROR)
{
size_t nb_rec = sock_err;
if (nb_rec > 0 )
{
data[nb_rec] = 0 ;
printf (" %u byte%s received:\n'%s'\n " ,
(unsigned ) nb_rec, nb_rec > 1 ? " s " : " " , data);
fflush (stdout);
if (data[0 ] = = ESC)
{
end = 1 ;
}
else
{
char const response[] = " OK\n " ;
send (p_cli- > sock, response, strlen (response), 0 );
}
}
else
{
puts (" client is disconnected " );
end = 1 ;
}
}
else
{
perror (" socket.recv " );
p_cli- > err = 1 ;
end = 1 ;
}
}
while (! end);
shutdown (p_cli- > sock, 2 );
printf (" closing client socket %d...\n " , p_cli- > sock);
closesocket (p_cli- > sock), p_cli- > sock = INVALID_SOCKET;
}
return NULL ;
}
static int clients (SOCKET sock)
{
int err = 0 ;
int end = 0 ;
do
{
printf (" waiting for a client connection on port %d...\n " , PORT);
{
struct cli * p_cli = malloc (sizeof * p_cli);
if (p_cli ! = NULL )
{
p_cli- > recsize = (int ) sizeof p_cli- > sin;
p_cli- > sock =
accept (sock, (SOCKADDR * ) & p_cli- > sin, & p_cli- > recsize);
if (p_cli- > sock ! = INVALID_SOCKET)
{
printf
(" client connected with socket %d from %s:%d\n " ,
p_cli- > sock, inet_ntoa (p_cli- > sin.sin_addr),
htons (p_cli- > sin.sin_port));
client (p_cli);
}
else
{
perror (" socket.accept " );
err = 1 ;
}
free (p_cli), p_cli = NULL ;
}
else
{
printf (" client creation failed : memory error\n " );
}
}
}
while (! end);
return err;
}
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin = { 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (PORT);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , PORT);
if (sock_err ! = SOCKET_ERROR)
{
err = clients (sock);
}
else
{
perror (" socket.listen " );
err = 1 ;
}
}
else
{
perror (" socket.bind " );
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Pour réaliser une serveur plus conséquent, on peut créer un thread (ou un processus) par client connecté.
VII-D-9. 08 - Serveur-multi-clients : Reception d'un bloc de données puis deconnexion sur ESC
En rendant autonome la fonction 'client()' et en la transformant en thread, le serveur devient multiclient.
En effet, après connexion d'un client, au lieu de rester bloqué sur la réception, le serveur se
bloque à nouveau sur l'attente de connexion (accept()). Ca permet de recevoir et de traiter plusieurs
demandes de connexions en même temps. Le serveur ne s'arrête jamais.
VII-D-9-a. Pseudo-code
STRUCTURE CLIENT
BEGIN
thread :PTHREAD
sock :SOCKET
END
THREAD client (data:GENERIC):GENERIC
BEGIN
cli := CLIENT (data)
WITH cli
BEGIN
REPEAT
recv (sock, data)
send (sock, " OK " )
UNTIL data[0 ] = ESC
closesocket (sock)
END
client := NIL
END
FUNCTION clients (sock:SOCKET)
BEGIN
DO
csock := accept (sock, csin)
pthread_create (cli.thread, client, cli.csock)
FOREVER
END
BEGIN
sock := socket ()
bind (sock, ALL_IP, 23 )
listen (sock, 5 )
clients (sock)
closesocket (sock)
END
|
Ce fonctionnement reste simple, et il accepte plusieurs connexions à la fois. Les fonctions accept()
et recv() sont bloquantes, mais elles sont maintenant dans des threads différents. Dès qu'un client
est crée, la fonction 'clients' revient en attente de connexion.
VII-D-9-b. Code
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
# include <pthread.h>
# define TELNET 23
# define PORT TELNET
# define ESC 27
struct cli
{
pthread_t thread;
SOCKADDR_IN sin;
int recsize;
SOCKET sock;
int err;
} ;
static void * client (void * p_data)
{
struct cli * p_cli = p_data;
if (p_cli ! = NULL )
{
int end = 0 ;
do
{
unsigned char data[128 ];
int sock_err = recv (p_cli- > sock, data, (sizeof data - 1 ), 0 );
if (sock_err ! = SOCKET_ERROR)
{
size_t nb_rec = sock_err;
if (nb_rec > 0 )
{
data[nb_rec] = 0 ;
printf (" %u byte%s received:\n'%s'\n " ,
(unsigned ) nb_rec, nb_rec > 1 ? " s " : " " , data);
fflush (stdout);
if (data[0 ] = = ESC)
{
end = 1 ;
}
else
{
char const response[] = " OK\n " ;
send (p_cli- > sock, response, strlen (response), 0 );
}
}
else
{
puts (" client is disconnected " );
end = 1 ;
}
}
else
{
perror (" socket.recv " );
p_cli- > err = 1 ;
end = 1 ;
}
}
while (! end);
shutdown (p_cli- > sock, 2 );
printf (" closing client socket %d...\n " , p_cli- > sock);
closesocket (p_cli- > sock), p_cli- > sock = INVALID_SOCKET;
free (p_cli), p_cli = NULL ;
}
return NULL ;
}
static int clients (SOCKET sock)
{
int err = 0 ;
int end = 0 ;
do
{
printf (" waiting for a client connection on port %d...\n " , PORT);
{
struct cli * p_cli = malloc (sizeof * p_cli);
if (p_cli ! = NULL )
{
p_cli- > recsize = (int ) sizeof p_cli- > sin;
p_cli- > sock =
accept (sock, (SOCKADDR * ) & p_cli- > sin, & p_cli- > recsize);
if (p_cli- > sock ! = INVALID_SOCKET)
{
printf
(" client connected with socket %d from %s:%d\n " ,
p_cli- > sock, inet_ntoa (p_cli- > sin.sin_addr),
htons (p_cli- > sin.sin_port));
pthread_create (& p_cli- > thread, NULL , client, p_cli);
p_cli = NULL ;
}
else
{
perror (" socket.accept " );
err = 1 ;
}
}
else
{
fprintf (stderr, " client creation failed : memory error\n " );
}
}
}
while (! end);
return err;
}
static int app (void )
{
int err = 0 ;
SOCKET sock = socket (AF_INET, SOCK_STREAM, 0 );
if (sock ! = INVALID_SOCKET)
{
printf (" socket %d is now opened in TCP/IP mode\n " , sock);
{
int sock_err;
SOCKADDR_IN sin = { 0 } ;
sin.sin_addr.s_addr = htonl (INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons (PORT);
sock_err = bind (sock, (SOCKADDR * ) & sin, sizeof sin);
if (sock_err ! = SOCKET_ERROR)
{
sock_err = listen (sock, 5 );
printf (" listening on port %d...\n " , PORT);
if (sock_err ! = SOCKET_ERROR)
{
err = clients (sock);
}
else
{
perror (" socket.listen " );
err = 1 ;
}
}
else
{
perror (" socket.bind " );
err = 1 ;
}
printf (" closing socket %d...\n " , sock);
sock_err = closesocket (sock), sock = INVALID_SOCKET;
printf (" the socket is now closed\n " );
if (sock_err)
{
perror (" socket.close " );
err = 1 ;
}
}
}
else
{
perror (" socket.open " );
err = 1 ;
}
return err;
}
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
err = app ();
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
system (" pause " );
return ret;
}
|
VII-D-10. Conclusion
Les base de la réalisation d'un mini serveur Telnet sont jetées. Il suffit maintenant de réconstituer
les lignes de commandes, de les transmettre à un interpréteur de commande etc.
Exemple de serveur multiclient simple avec client qui demande l'heure courante (now) ou quitte (quit)
:
VII-E. Transmettre du texte par sockets
Un socket permet de transmettre des données 'brutes' sous la forme de blocs d'octets caractérisés dans
le code par une adresse et une longueur (les paramètres de send(), par exemple).
Des informations de type texte peuvent évidemment se ramener à une structure adresse, longueur. Mais
il convient de le faire proprement et en respectant certaines conventions telles que
-
Le jeu de caractères est ASCII
-
Pas de 0 en ligne
-
Les chaines sont terminées par une marque de fin de ligne telle que CR, LF, CRLF ou autre, définie par
protocole utilisé.
Le respect de ces conventions permet d'élaborer des fonctions d'émission et reception de texte correctes
et efficaces.
|
Attention : la technique utilisée dans ce qui suit n'est valide que sous Windows (winsock2).
|
VII-E-1. Emission de petits blocs (cas simple)
Par 'petit blocs', on entend des chaines dont la longueur est inférieure à une trame (quelques centaines
d'octets). Le principe est donc de passer l'adresse du premier élément du tableau de char qui contient
la chaine, et sa longueur mesurée ici avec strlen().
La fonction send() retourne le nombre d'octets effectivement transmis
char * text = " Hello world\n " ;
int n = send (sock, text, strlen (text), 0 );
if (n >= 0 )
{
printf (" %d octet%s sent\n " , n, n ! = 1 ? " s " : " " );
}
|
VII-E-2. Reception de petits blocs
La réception doit tenir compte du fait qu'en C, les chaines sont terminées par un 0. Il faut donc, non
seulement placer ce 0 "à la main" (puisqu'il n'est pas transmis), mais en plus prévoir de la place
pour lui dans la chaine de reception.
La fonction recv() retourne le nombre d'octets effectivement reçus. Cette valeur est <= à la taille
passée en paramètre. Elle sert à positionner le 0. Une valeur < 0 signale une erreur. 0 signifie
'rien reçu' dans le cas d'un socket non blocant. (ne devrait pas exister si le socket est blocant).
char text[128 ];
int n = recv (sock, text, sizeof text - 1 , 0 );
if (n > 0 )
{
text[n] = 0 ;
printf (" received : '%s'\n " , text);
}
|
VII-E-3. Emission de gros blocs (cas général)
Pour émettre un gros bloc, il faut utiliser une technique de découpage si on ne veut pas perdre de données.
Etant donné qu'en général on ne sait pas quelle est la valeur maximale d'une trame sur la machine,
le mieux est de concevoir du code auto-adaptatif. Pour cela, on utilise une information capitale
qui est la valeur retournée par send(). En effet, la valeur retournée est le nombre d'octets effectivement
émis.
Par exemple, si la trame maximale fait 1024 octets et qu'on demande à en émettre 2000, send() va retourner
1024. A nous de créer un algorithme qui tient compte de ce résultat et envoie la suite des données.
size_t const len = strlen (text);
size_t sent = 0 ;
while (sent ! = len)
{
int n = send (socket, text + sent, len - sent, 0 );
if (n >= 0 )
{
sent + = n;
}
else
{
}
}
|
Cette technique est générale et couvre évidemment le cas des petits blocs. Il serait intéressant de placer
ce code dans une fonction.
VII-E-4. Réception de gros blocs
La réception assez simple. Il suffit de stocker les blocs reçus et de les passer à une fonction de traitement
des données.
Etant donné que l'émission peut avoir découpé les informations en trames plus petites que les informations
à transmettre, il faut veiller à ce que les données soient 'réassemblées' sous la forme de lignes
cohérentes, c'est à dire terminées par un '\n' si on cherche à interpéter les données reçues.
Les données reçues après un '\n' ne doivent pas être éliminées, mais sont le début de la igne suivante.
Un mécanisme de réconstitution des lignes doit donc être mis en place en se basant sur la marque de fin
de ligne. Si le programme ne fait que router les donnée (par exemple, un serveur de connexion client/client
ou une transmission de fichier texte), il pourra se contenter de retransmettre les données inchangées.
VII-F. Etude de cas : transmettre un fichier texte.
VII-F-1. Spécifications
Soit à réaliser un logiciel qui transmet un fichier nommé "fichier.txt" contenant un nombre non spécifié
de lignes de textes correctement formées (CR, LF, CRLF ou LFCR).
Nota : l'accent est mis sur le protocole et l'integrité des données, et non sur les fonctionnalités
qui sont réduites au strict nécessaire.
VII-F-2. Conception
Le logiciel est composé de deux programmes.
-
L'un tourne sur un serveur. Il reçoit la commande de transmission de la part d'un utilisateur et il fourni
le fichier à transmettre. C'est le serveur.
-
L'autre tourne sur une machine utilisateur. Il émet la demande de transmission et reçoit le fichier qu'il
stocke localement : c'est le client.
VII-F-2-a. Protocole
Afin de garantir l'intégrité du transfert, le protocole suivant est établi
-
Un message est une ligne de texte. La fin de ligne peut être CR, LF, CRLF ou LFCR.
-
Le messages peuvent être des commandes ou des réponses.
-
Le client a l'initiative du transert. C'est lui qui étalit la connexion avec le serveur et qui transmet
la commande de récupération de fichier.
-
Le serveur répond alors "OK" ou "KO", Si la réponse est OK, le transfert peut commencer dès que le client
passe une commande d'exécution. Le transfert se termine lorsque la dernière ligne a été transmise.
La connexion est alors fermée par le serveur.
VII-F-2-a-i. Format des messages
message ::= < texte> < EOL>
texte ::= sequence of printable characters (ASCII)
EOL ::= CR | LF | CRLF | LFCR
|
VII-F-2-a-ii. Commandes
Le client peut passer les commandes suivantes
VII-F-2-a-iii. Réponses
Le serveur peut émettre les réponses suivantes
VII-F-2-a-iv. Exemple de dialogue
Client Serveur
- - - - - - connexion - - - - - - - - - - - >
- - - - - - " get fichier.txt " - - - >
< - - - - - " ok " - - - - - - - - - - - - - - - - -
fopen (..., " w " )
- - - - - - " go " - - - - - - - - - - - - - - - - >
fopen (..., " r " )
fgets () send ()
< - - - - - donnees - - - - - - - - - - - - - -
recv ()
fputs ()
fopen (..., " r " )
fgets () send ()
< - - - - - donnees - - - - - - - - - - - - - -
recv ()
fputs ()
< - - - - - ... - - - - - - - - - - - - - - - - - -
fclose ()
< - - - - - deconnexion - - - - - - - - - -
fclose ()
|
Ce protocole basique devrait fonctionner, mais le fait de compter sur la déconnexion pour fermer le fichier
en écriture est un peu risqué. Il y a plusieurs façons d'améliorer le protocole :
-
Transmettre le nombre de lignes attendues et fermer quand la dernière ligne a été reçue.
-
Acquitter chaque ligne par un "OK". La dernière est acquittée par un "EOF".
VII-G. Du bon usage de select()
VII-G-1. Introduction
La fonction
select()
suspend l'exécution de la tache courante. Celle-ci reprend si au moins une des conditions suivantes
est vérifiée :
-
Réception sur un flux (sauf Windows) ou un socket.
-
Fin d'émission sur un flux (sauf Windows) ou un socket.
-
Réception d'un message d'erreur sur un flux (sauf Windows) ou un socket.
-
Echéance de temps.
L'analyse de la valeur retournée permet en partie d'identifier l'évènement. (-1 = erreur, 0 = echéance,
autre = évènement flux)
VII-G-2. Programmation
Chacune des conditions est programmable individuellement. On utilise pour cela les paramètres de la fonction,
qui, il faut le reconnaitre, paraissent un peu étranges au début. En fait tout cela est extrêmement
simple et logique :
Le premier paramètre est le plus grand numéro de flux surveillé augmenté de 1.
Le deuxième paramètre est un pointeur sur un objet de type fd_set qui contient
la liste des flux entrants surveillés (réception de données)
Le troisième paramètre est un pointeur sur un objet de type fd_set qui contient
la liste des flux sortants surveillés (émission de données)
Le quatrième paramètre est un pointeur sur un objet de type fd_set qui contient
la liste des flux entrants surveillés (réception d'un message hors bandes : supervision)
Le cinquième parametre est l'adresse d'un objet de type struct timeval qui
contient la durée max de la suspension (timeout).
VII-G-3. Exemples d'utilisation
Ces exemples, un peu scolaires, montrent une possiblité à la fois. Lorsqu'un paramètre n'est pas utilisé,
on lui donne la valeur 0 ou NULL.
VII-G-3-a. Suspension temporisée
Il suffit de regler la valeur de la temporisation. Attention, pour être portable, celle-ci doit être
mise à jour à chaque fois, car il est possible que les valeurs de la structure soient altérées
par select().
for (;;)
{
struct timeval timeout;
timeout.tv_sec = 1 ;
timeout.tv_usec = 5 * 100 * 1000 ;
int err = select (0 , NULL , NULL , NULL , & timeout);
switch (err)
{
case 0 :
puts (" timeout " );
break ;
case - 1 :
puts (" error " );
break ;
default :
}
}
|
VII-G-3-b. Surveillance d'un flux en réception
Cet exemple est purement scolaire, car il n'a pas d'intérêt fonctionnel, étant donné que les fonctions
recv() et recvfrom() sont blocantes par défaut. Mais il permet de montrer la syntaxe sur un cas
simple. Les macros FD_ZERO() et FD_SET()
facilitent la manipulation de l'objet fd_set.
for (;;)
{
fd_set readfs;
FD_ZERO (& readfs);
FD_SET (sock, & readfs);
int err = select (sock + 1 , & readfs, NULL , NULL , NULL );
switch (err)
{
case 0 :
break ;
case - 1 :
puts (" error " );
break ;
default :
if (FD_ISSET (sock, & readfs))
{
char data[128 ];
int n = recv (sock, data, sizeof data, 0 );
if (n > 0 )
{
}
}
}
}
|
VII-G-3-c. Surveillance de deux flux en réception
Cet exemple est réel. Il permet de suspendre l'exécution en un endroit précis et unique du logiciel et
donc de surveiller la réception de n flux (ici, 2). FD_SETSIZE retourne
une valeur correcte mais non optimisée pour le premier paramètre de select().
for (;;)
{
fd_set readfs;
FD_ZERO (& readfs);
FD_SET (sock_a, & readfs);
FD_SET (sock_b, & readfs);
int err = select (FD_SETSIZE, & readfs, NULL , NULL , NULL );
switch (err)
{
case 0 :
break ;
case - 1 :
puts (" error " );
break ;
default :
if (FD_ISSET (sock_a, & readfs))
{
char data[128 ];
int n = recv (sock_a, data, sizeof data, 0 );
if (n > 0 )
{
}
}
if (FD_ISSET (sock_b, & readfs))
{
char data[128 ];
int n = recv (sock_b, data, sizeof data, 0 );
if (n > 0 )
{
}
}
}
}
|
|
L'usage de FD_SETSIZE pour un serveur surveillant quelques sockets peut s'avérer pénalisant. Dans ce
cas, il est préférable de s'en tenir à la définition, c'est à dire la valeur max des sockets surveillés
augmentée de 1 :
|
int max_sock = MAX (sock_a, sock_b);
select (max_sock + 1 , );
|
VII-G-4. Conclusion
Les bases sont posées. Il ne reste plus maintenant qu'à faire jouer son imagination pour combiner les
possibilités en fonction des besoins. Au cours de la conception, ne pas oublier les possibilités
offertes par les threads.
VII-H. La récupération de l'IP à partir du nom du serveur
A chaque serveur est attribué une ou plusieurs IP publiques sur le réseau mondial Internet. Pour communiquer
avec tel ou tel serveur, l'usage des sockets implique de connaitre une de ces IP publiques. On utilise
pour ça la fonction gethostbyname() et les éléments de la structure récupérée (h_*).
# ifdef __cplusplus
# error Be sure you are using a C compiler . . .
# endif
# if defined ( WIN32 )
# include <winsock2.h>
# elif defined ( linux ) | | defined ( _POSIX_VERSION ) | | defined ( _POSIX2_C_VERSION ) \
| | defined (_XOPEN_VERSION)
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h> /* close() */
# include <netdb.h> /* gethostbyname() */
# define INVALID_SOCKET - 1
# define SOCKET_ERROR - 1
# define closesocket ( s ) close ( s )
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
# else
# error not defined for this platform
# endif
# include <stdio.h>
# include <stdlib.h>
int main (void )
{
int ret;
# if defined ( WIN32 )
WSADATA wsa_data;
int err = WSAStartup (MAKEWORD (2 , 2 ), & wsa_data);
if (! err)
{
puts (" WIN: winsock2: OK " );
# else
int err;
# endif
{
struct hostent * p = gethostbyname (" www.google.com " );
if (p ! = NULL )
{
struct hostent host = * p;
printf (" IP de %s\n " , host.h_name);
{
int i = 0 ;
char * ip;
while ((ip = host.h_addr_list[i]) ! = NULL )
{
int j;
for (j = 0 ; j < host.h_length; j+ + )
{
if (j > 0 )
{
putchar (' . ' );
}
printf (" %u " , (unsigned ) (unsigned char ) ip[j]);
}
putchar (' \n ' );
i+ + ;
}
}
}
}
# if defined ( WIN32 )
WSACleanup ();
}
# endif
if (err)
{
ret = EXIT_FAILURE;
}
else
{
ret = EXIT_SUCCESS;
}
return ret;
}
|
Ce qui donne :
WIN : winsock2: OK
IP de www.l.google.com
216 .239 .59 .103
216 .239 .59 .104
216 .239 .59 .147
216 .239 .59 .99
Press ENTER to continue .
|
On constate que le véritable nom de serveur de google est "www.l.google.com" et que, vu de chez moi,
il possède 4 IP publiques.
VII-I. Ebauche d'une bibliothèque sockets portable (psock)
VII-I-1. Interfaces communes
VII-I-2. Sockets UNix (sun)
VII-I-3. Sockets IP (Internet Protocol)
VII-J. Ressources
-
Le site d' IBM
-
Le tutoriel rigolo mais bien fourni de Brian
"Beej" Hall (en français)
-
L'excellent tutoriel de Benjamin Roux
de Developpez.com.
-
L'excellent logiciel libre multi-plateformes d'analyse de réseau WireShark
(ex-EtherReal)
Copyright © 2008 Emmanuel Delahaye.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.