GTS : Generic Tcp Server
Nous allons réaliser en C un serveur tcp/ip multi-clients générique…
oulà… doucement :
-serveur : va attendre que des clients se connectent
-tcp/ip : les connections clientes se feront par le protocole tcp/ip
-multi-clients : le serveur peut gérer plusieurs clients en même temps
-générique 😮 : le serveur s’occupe uniquement de l’aspect communications réseaux. Lorsqu’un client se connecte, il exécute une commande shell de notre choix.
On va voir quelques exemples pour bien comprendre :
exemple 1 :
09:57:33 apesle:~ $ gts "echo $SHELL" 1234 &
[1] 28862
Je lance notre programme gts en tache de fond. Je lui demande d’attendre des clients sur le port 1234 et, pour chaque client, d’exécuter la commande « echo $SHELL ».
09:57:50 apesle:~ $ nc localhost 1234
/bin/bash
09:58:03 apesle:~ $ nc localhost 1234
/bin/bash
Avec NetCat, je créé deux clients sur le port 1234 qui reçoivent bien le nom du shell utilisé.
exemple 2 :
09:58:03 apesle:~ $ gts "echo $PWD" 1235 &
[2] 28872
Je lance un nouveau processus gts en tache de fond. Cette fois ci je lui demande d’attendre des clients sur le port 1235 et, pour chaque client, d’exécuter la commande « echo $PWD ».
09:58:16 apesle:~ $ nc localhost 1235
/home/apesle
On peut voir que le client reçoit bien le répertoire courant du shell.
exemple 3 :
10:08:56 apesle:~ $ gts "cd ~/Desktop/tests && ./anagramme" 1236 &
Je créé un serveur sur le port 1236 qui lance notre programme C d’anagrammes pour chaque client.
10:09:26 apesle:~ $ nc localhost 1236
nuuuunnnix
uuxunnunin
generic tcp server
grrcrs ee cievtpen
^C
Le client accède bien au programme d’anagramme à travers la connection tcp/ip.
exemple 4 :
09:58:20 apesle:~ $ gts "nc localhost 1234" 1237 &
[3] 28885
Cette fois ci c’est plus compliqué : je demande au serveur de s’établir sur le port 1237, et lorsqu’il reçoit un client d’exécuter la commande « nc localhost 1234 » qui créé en fait un client pour notre premier serveur.
09:59:06 apesle:~ $ nc localhost 1237
/bin/bash
Le client reçoit donc le nom du shell utilisé ! En effet, on s’est connecté au serveur, le serveur a donc exécuté la commande « nc localhost 1234 » qui créé un client pour le serveur de l’exemple 1. Le serveur de l’exemple 1 a donc exécuté la commande « echo $SHELL » et envois le résultat à notre serveur, qui l’envois à notre client !
Avec la commande « netstat -tln », on peut voir nos 4 serveurs en attente de connections tcp/ip :
tcp 0 0 0.0.0.0:1234 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:1235 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:1236 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:1237 0.0.0.0:* LISTEN
Bon, mais comment ça marche ?
Le principe de fonctionnement est le suivant :
- le programme créé une socket serveur, puis dans une boucle infinie accepte les clients qui s’y connectent.
- À chaque fois qu’un client se connecte, on créé un nouveau processus avec un « fork ». C’est ce processus fils qui va traiter la communication, alors que le père retourne en attente d’un client.
- On a donc un processus fils par client. Le processus fils redirige ses entrées/sorties standards sur la socket ouverte par son père puis exécute un programme tierce grâce à un « exec ».
Le rôle du serveur est donc uniquement de gérer les sockets. Il délègue la communication avec les clients à un programme tierce qui manipule uniquement stdin et stdout.
Ce programme peut, par ailleurs, être écrit en n’importe quel langage (c,c++, script shell, etc).
Ok, et le source alors ?
Le programme, bien qu’assez succint, est intéressant.
Il montre l’utilisation des sockets en mode tcp/ip (notamment l’option SO_REUSEADDR qui permet d’éviter l’erreur « bind: Address already in use »), la création de processus avec fork, la redirection de flux standard avec dup, ainsi que la mise en place d’un gestionnaire de signaux.
Le code source est commenté, cependant quelques explications au niveau de la gestion des signaux n’est peut être pas de trop :
Le programme met en place un gestionnaire de signaux pour SIGCHLD (signal émis par le noyau lors de la terminaison d’un processus fils).
Ce signal, reçut par le processus gts, interrompt l’appel système accept (qui permet d’accepter des connections clientes sur la socket). On utilise donc errno pour détecter ce cas et ré-appeler accept.
Notez également que le gestionnaire de signaux mis en place pour SIGCHLD lit le code de retour du processus fils terminé pour éviter qu’il ne reste à l’état zombie (état spécial d’un processus qui attend indéfiniment que son père lise son code de retour).
PS :
-Pour compiler : gcc -Wall -g -o gts gts.c
-Installer : sudo cp gts /usr/bin/
{codecitation class= »brush: c; »}
/*
Copyright (c) 2009 Adrien Pesle - apesle at nunix.fr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
extern char **environ;
int
cree_socket_stream (const unsigned int port)
{
int sock;
struct sockaddr_in adresse;
memset (&adresse, 0, sizeof (struct sockaddr_in));
/* création de la socket */
if ((sock = socket (AF_INET, SOCK_STREAM, 0)) < 0)
{
perror (« socket »);
return -1;
}
/* appliquer l’option SO_REUSEADDR qui dit au kernel de réutiliser
le port même si il est occupé (état TIME_WAIT). Ceci permet
d’éviter l’erreure « bind: Address already in use » */
int yes = 1;
if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof (int)) == -1)
{
perror (« Impossible d’appliquer setsockopt avec SO_REUSEADDR ! »);
return -1;
}
/* paramétrage */
adresse.sin_family = AF_INET;
adresse.sin_port = htons (port);
adresse.sin_addr.s_addr = htonl (INADDR_ANY);
if (bind
(sock, (struct sockaddr *) &adresse, sizeof (struct sockaddr_in)) < 0)
{
close (sock);
perror (« bind »);
return -1;
}
return sock;
}
void
traite_connexion (int sock, const char *commande)
{
/* redirection de stdout et stdin sur la socket */
close (STDOUT_FILENO);
if (dup (sock) < 0)
{
perror (« dup »);
exit (EXIT_FAILURE);
}
close (STDIN_FILENO);
if (dup (sock) < 0)
{
perror (« dup »);
exit (EXIT_FAILURE);
}
/* execution de la commande passée en paramètre du serveur */
char *argv[] = { « sh », « -c », (char *) commande, (char *) NULL };
execve (« /bin/sh », argv, environ);
fprintf (stdout, « Raté : erreur = %dn », errno);
close (sock);
}
int
serveur_tcp (const char *commande, const unsigned int port)
{
int sock_contact;
int sock_connectee;
struct sockaddr_in adresse;
socklen_t longueur;
sock_contact = cree_socket_stream (port);
if (sock_contact < 0)
return -1;
listen (sock_contact, 5);
while (1)
{
longueur = sizeof (struct sockaddr_in);
sock_connectee =
accept (sock_contact, (struct sockaddr *) &adresse, &longueur);
if (sock_connectee == -1)
{
if (EINTR == errno)
/* l’appel system a été interrompu à cause du signal SIGCHLD
provoqué par la déconnection d’un client. */
continue;
else
{
/* l’appel system accept a échoué */
perror (« accept »);
return -1;
}
}
/* création d’un processus fils pour chaque client */
switch (fork ())
{
case 0: /* fils */
close (sock_contact);
traite_connexion (sock_connectee, commande);
exit (EXIT_SUCCESS);
case -1: /* erreure */
perror (« fork »);
return -1;
default: /* père */
close (sock_connectee);
}
}
return 0;
}
static void
gestionnaire_signaux (int ignore)
{
/* lecture du code de retour du fils terminé pour eviter
qu’il ne reste zombi. On passe NULL car on n’est pas
intérréssé par les circonstances de la fin du processus. */
wait (NULL);
}
int
main (int argc, char *argv[])
{
if (argc < 3)
{
printf (« Usage : %s commande portn », argv[0]);
printf
(« comande = commande exécutée par le shell pour chaque client.n »);
printf (« port = port d’écoute.n »);
exit (EXIT_FAILURE);
}
/*———————————————————————–
* gestionnaire de signaux qui capture SIGGHLD pour éviter que les
* processus fils deviennent zombi.
*———————————————————————–*/
struct sigaction action;
action.sa_handler = gestionnaire_signaux;
sigemptyset (&(action.sa_mask));
action.sa_flags = 0;
if (sigaction (SIGCHLD, &action, NULL) != 0)
{
fprintf (stderr, « Signal non capturé.n »);
exit (EXIT_FAILURE);
}
return serveur_tcp (argv[1], atoi (argv[2]));
}
{/codecitation}
{jcomments on}