Publi

Creando un servidor que acepte múltiples clientes simultáneos en C

5989707041_64de393f6b_oPara hacer una prueba de esto, crearemos un servidor al que nos podremos conectar por telnet y pedir cierta información a través de comandos. El ejemplo soporta los siguientes comandos (en mayúsculas):

  • DATE: Pide la fecha al servidor
  • TIME: Pide la hora al servidor
  • HOLA: Saluda y me dice mi IP
  • EXIT: Cierra el cliente actual
  • CERRAR: Cierra el servidor.
  • Cualquier otra cosa se manda repetida (ECHO)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
*************************************************************
* @file servtcp.c
* @brief Breve descripción
* Ejemplo de un cliente TCP usando threads
*
*
* @author Gaspar Fernández <blakeyed@totaki.com>
* @version 0.1Beta
* @date 13 ene 2011
* Historial de cambios:
*   20110113 - Versión inicial
*
*
*************************************************************/


#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <netinet/in.h>
#include <resolv.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

/** Puerto  */
#define PORT       7000

/** Número máximo de hijos */
#define MAX_CHILDS 3

/** Longitud del buffer  */
#define BUFFERSIZE 512

int AtiendeCliente(int socket, struct sockaddr_in addr);
int DemasiadosClientes(int socket, struct sockaddr_in addr);
void error(int code, char *err);
void reloj(int loop);

int main(int argv, char** argc){

    int socket_host;
    struct sockaddr_in client_addr;
    struct sockaddr_in my_addr;
    struct timeval tv;      /* Para el timeout del accept */
    socklen_t size_addr = 0;
    int socket_client;
    fd_set rfds;        /* Conjunto de descriptores a vigilar */
    int childcount=0;
    int exitcode;

    int childpid;
    int pidstatus;

    int activated=1;
    int loop=0;
    socket_host = socket(AF_INET, SOCK_STREAM, 0);
    if(socket_host == -1)
      error(1, "No puedo inicializar el socket");
   
    my_addr.sin_family = AF_INET ;
    my_addr.sin_port = htons(PORT);
    my_addr.sin_addr.s_addr = INADDR_ANY ;

   
    if( bind( socket_host, (struct sockaddr*)&my_addr, sizeof(my_addr)) == -1 )
      error(2, "El puerto está en uso"); /* Error al hacer el bind() */

    if(listen( socket_host, 10) == -1 )
      error(3, "No puedo escuchar en el puerto especificado");

    size_addr = sizeof(struct sockaddr_in);


    while(activated)
      {
    reloj(loop);
    /* select() se carga el valor de rfds */
    FD_ZERO(&rfds);
    FD_SET(socket_host, &rfds);

    /* select() se carga el valor de tv */
    tv.tv_sec = 0;
    tv.tv_usec = 500000;    /* Tiempo de espera */
   
    if (select(socket_host+1, &rfds, NULL, NULL, &tv))
      {
        if((socket_client = accept( socket_host, (struct sockaddr*)&client_addr, &size_addr))!= -1)
          {
        loop=-1;        /* Para reiniciar el mensaje de Esperando conexión... */
        printf("\nSe ha conectado %s por su puerto %d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
        switch ( childpid=fork() )
          {
          case -1:  /* Error */
            error(4, "No se puede crear el proceso hijo");
            break;
          case 0:   /* Somos proceso hijo */
            if (childcount<MAX_CHILDS)
              exitcode=AtiendeCliente(socket_client, client_addr);
            else
              exitcode=DemasiadosClientes(socket_client, client_addr);

            exit(exitcode); /* Código de salida */
          default:  /* Somos proceso padre */
            childcount++; /* Acabamos de tener un hijo */
            close(socket_client); /* Nuestro hijo se las apaña con el cliente que
                         entró, para nosotros ya no existe. */

            break;
          }
          }
        else
          fprintf(stderr, "ERROR AL ACEPTAR LA CONEXIÓN\n");
      }

    /* Miramos si se ha cerrado algún hijo últimamente */
    childpid=waitpid(0, &pidstatus, WNOHANG);
    if (childpid>0)
      {
        childcount--;   /* Se acaba de morir un hijo */

        /* Muchas veces nos dará 0 si no se ha muerto ningún hijo, o -1 si no tenemos hijos
         con errno=10 (No child process). Así nos quitamos esos mensajes*/


        if (WIFEXITED(pidstatus))
          {

        /* Tal vez querremos mirar algo cuando se ha cerrado un hijo correctamente */
        if (WEXITSTATUS(pidstatus)==99)
          {
            printf("\nSe ha pedido el cierre del programa\n");
            activated=0;
          }
          }
      }
    loop++;
    }

    close(socket_host);

    return 0;
}

    /* No usamos addr, pero lo dejamos para el futuro */
int DemasiadosClientes(int socket, struct sockaddr_in addr)
{
    char buffer[BUFFERSIZE];
    int bytecount;

    memset(buffer, 0, BUFFERSIZE);
   
    sprintf(buffer, "Demasiados clientes conectados. Por favor, espere unos minutos\n");

    if((bytecount = send(socket, buffer, strlen(buffer), 0))== -1)
      error(6, "No puedo enviar información");
   
    close(socket);

    return 0;
}

int AtiendeCliente(int socket, struct sockaddr_in addr)
{

    char buffer[BUFFERSIZE];
    char aux[BUFFERSIZE];
    int bytecount;
    int fin=0;
    int code=0;         /* Código de salida por defecto */
    time_t t;
    struct tm *tmp;

    while (!fin)
      {

    memset(buffer, 0, BUFFERSIZE);
    if((bytecount = recv(socket, buffer, BUFFERSIZE, 0))== -1)
      error(5, "No puedo recibir información");

    /* Evaluamos los comandos */
    /* El sistema de gestión de comandos es muy rudimentario, pero nos vale */
    /* Comando TIME - Da la hora */
    if (strncmp(buffer, "TIME", 4)==0)
      {
        memset(buffer, 0, BUFFERSIZE);

        t = time(NULL);
        tmp = localtime(&t);

        strftime(buffer, BUFFERSIZE, "Son las %H:%M:%S\n", tmp);
      }
    /* Comando DATE - Da la fecha */
    else if (strncmp(buffer, "DATE", 4)==0)
      {
        memset(buffer, 0, BUFFERSIZE);

        t = time(NULL);
        tmp = localtime(&t);

        strftime(buffer, BUFFERSIZE, "Hoy es %d/%m/%Y\n", tmp);
      }
    /* Comando HOLA - Saluda y dice la IP */
    else if (strncmp(buffer, "HOLA", 4)==0)
      {
        memset(buffer, 0, BUFFERSIZE);
        sprintf(buffer, "Hola %s, ¿cómo estás?\n", inet_ntoa(addr.sin_addr));
      }
    /* Comando EXIT - Cierra la conexión actual */
    else if (strncmp(buffer, "EXIT", 4)==0)
      {
        memset(buffer, 0, BUFFERSIZE);
        sprintf(buffer, "Hasta luego. Vuelve pronto %s\n", inet_ntoa(addr.sin_addr));
        fin=1;
      }
    /* Comando CERRAR - Cierra el servidor */
    else if (strncmp(buffer, "CERRAR", 6)==0)
      {
        memset(buffer, 0, BUFFERSIZE);
        sprintf(buffer, "Adiós. Cierro el servidor\n");
        fin=1;
        code=99;        /* Salir del programa */
      }
    else
      {    
        sprintf(aux, "ECHO: %s\n", buffer);
        strcpy(buffer, aux);
      }

    if((bytecount = send(socket, buffer, strlen(buffer), 0))== -1)
      error(6, "No puedo enviar información");
      }

    close(socket);
    return code;
}

void reloj(int loop)
{
  if (loop==0)
    printf("[SERVIDOR] Esperando conexión  ");

  printf("\033[1D");        /* Introducimos código ANSI para retroceder 2 caracteres */
  switch (loop%4)
    {
    case 0: printf("|"); break;
    case 1: printf("/"); break;
    case 2: printf("-"); break;
    case 3: printf("\"); break;
    default:            /* No debemos estar aquí */
      break;
    }

  fflush(stdout);       /* Actualizamos la pantalla */
}

void error(int code, char *err)
{
  char *msg=(char*)malloc(strlen(err)+14);
  sprintf(msg, "
Error %d: %s\n", code, err);
  fprintf(stderr, msg);
  exit(1);
}

La clave para poder atender varios clientes simultáneos es bifurcar (fork()) el programa cuando se conecta un nuevo cliente, de esta forma, el diálogo con el cliente se lleva a cabo desde un nuevo proceso mientras que el proceso padre (el primero) sigue escuchando en el puerto esperando conexiones.

Utilizamos select() para no detener la ejecución del programa cuando estamos esperando conexiones; tal vez el programa tenga que hacer algo mientras nadie está conectándose (y es que el programa principal permanecerá aquí la mayor parte del tiempo), por ejemplo lo que hacemos aquí es controlar los hijos que se mueren y ver cómo se han muerto. select() establece un tiempo máximo de espera (500000 microseguntos en este caso, ver la variable tv); si pasado ese tiempo el descriptor socket_host no ha cambiado, seguiremos la ejecución normal del programa, pero si durante ese tiempo cambia ejecutaremos accept().
La función select() funciona con grupos de descriptores y por eso utilizamos las macros FD_ZERO() para limpiar el grupo y FD_SET() para añadir un descriptor al grupo.
El primer parámetro de select() debe ser el descriptor de fichero más grande a comprobar+1; como sólo tenemos un descriptor (socket_host) debemos añadir este al grupo, y pasarle a select en su primer parámetro socket_host+1.

Cuando llega un cliente, como dijimos antes, se abrirá un proceso hijo que lo procesará con una de estas dos funciones: AtiendeCliente() o DemasiadosClientes() dependiendo de si el número máximo de clientes ha sido alcanzado o no. Además, la salida del proceso hijo se obtendrá de la salida de estas funciones, siendo el valor de salida clave 99 el que necesita el proceso padre para cerrar terminar el proceso servidor, es decir, si un hijo sale con el valor de retorno 99 provocará el cierre del servidor. Este valor se maneja debajo del switch(); primero con waitpid() esperamos la muerte de alguno de nuestros hijos aunque con el modificador WNOHANG provocamos que si no se ha muerto ninguno, no vamos a esperar, el programa seguirá su ejecución.

Desde AtiendeCliente() veremos un método muy rudimentario para implementar los comandos, aunque para un ejemplo y 4 comandos nos vale perfectamente.

DemasiadosClientes() devolverá únicamente un mensaje al cliente que se acaba de conectar avisando de que hay demasiados clientes conectados.

Reloj() es un pequeño reloj en modo texto que va girando, a cada iteración del bucle principal, está aquí para que nosotros veamos que el servidor está haciendo algo. Utiliza códigos ANSI, para volver a escribirse en el lugar que estaba, y fflush() para volcar el texto a pantalla antes de nada (puede ser que se acumulen varios mensajes y entonces el dibujado sería incorrecto).

Para probar este programa, debemos compilar con:

$ gcc -o servtcp servtcp.c

y ejecutarlo; y, en un terminal aparte, conectar con telnet, Si, por ejemplo lo queremos hacer todo desde el mismo ordenador; si el servidor está en un ordenador diferente y nos queremos conectar a él cambiamos «localhost» por la IP o el host del ordenador donde se esté ejecutando el cliente:

$ telnet localhost 7000

Foto principal: Seeweb

También podría interesarte....

There are 25 comments left Ir a comentario

  1. Pingback: Bitacoras.com /

  2. Pingback: BlogESfera.com /

  3. Fancisco /
    Usando Mozilla Firefox Mozilla Firefox 16.0 en Ubuntu Linux Ubuntu Linux

    Esta muy limpio el codigo, pero al compilarlo en la consola de linuz ubuntu12.04 me da el siguiente error
    265:3: aviso: el formato no es una cadena literal y no tiene argumentos de formato [-Wformat-security]

    tal vez es que no se ha declarado la variable FILE,pero aun asi da el mismo error.
    Alguien tiene idea de por que pasa esto.

    1. admin / Post Author
      Usando Mozilla Firefox Mozilla Firefox 17.0 en Ubuntu Linux Ubuntu Linux

      Gracias Francisco.

      En la línea del fprintf (), la antepenúltima pon:

      fprintf(stderr, «%s», msg);

      Con eso no debería darte el fallo.

  4. Yeison /
    Usando Google Chrome Google Chrome 28.0.1500.52 en Linux Linux

    Aun hay alguien por aca?

  5. admin / Post Author
    Usando Mozilla Firefox Mozilla Firefox 22.0 en Ubuntu Linux Ubuntu Linux

    @Yeison
    Aunque un poco tarde, pero si, me asomo por aquí de vez en cuando 🙂

  6. Gerardo /
    Usando Mozilla Firefox Mozilla Firefox 23.0 en Linux Linux

    Hola.
    Gracias por el código.

    Solo tengo una duda:

    Osea que ésto teoricamente atendería 3 clientes al mismo tiempo? es decir, por ejemplo: si el servidor recibiera archivos, podría recibir 3 archivos 1 por cada cliente al mismo tiempo?

    Saludos 😀

    1. admin / Post Author
      Usando Mozilla Firefox Mozilla Firefox 22.0 en Ubuntu Linux Ubuntu Linux

      Exacto, de todas formas, siempre podemos ampliar el valor de MAX_CHILDS para que se pudieran atender más peticiones al mismo tiempo creando nuevos procesos hijos que las atiendan.

  7. Calos /
    Usando Google Chrome Google Chrome 28.0.1500.71 en Ubuntu Linux Ubuntu Linux

    Hola…
    Tengo una duda… como sería el codigo del cliente, modifiqué un poco este codigo para que resuelva un dominio con gethostbyname(); pero aun no se bien como conectar un clientes sin usar telnet.

    Gracias.

  8. admin / Post Author
    Usando Mozilla Firefox Mozilla Firefox 24.0 en Ubuntu Linux Ubuntu Linux

    @Calos
    Me has dado una buena idea para un futuro post, porque veo que es algo muy largo de explicar en un comentario. Espero tener un rato para publicarlo pronto. Muchas gracias !

  9. Pingback: Cliente TCP no bloqueante en C, en el que podemos enviar y recibir en cualquier momento | Poesía Binaria /

  10. Andres Felipe /
    Usando Google Chrome Google Chrome 35.0.1916.114 en Mac OS X Mac OS X 10.9.0

    @Calos
    Hola, el código sería algos así:
    #include
    #include
    #include
    #include
    #include
    #include
    #include

    #define MAXLINE 512 /*Longitud máxima de mensaje*/
    #define SERV_PORT 5728 /*puerto servidor*/

    int
    main(int argc, char **argv)
    {
    int sockfd;
    struct sockaddr_in servaddr;
    char sendline[MAXLINE], recvline[MAXLINE];

    if (argc !=2) {
    perror(«No se indicó la dirección IP del Servidor»);
    exit(1);
    }

    //Creamos socket para el cliente
    //Si sockfd<0 hay un error en la creación del socket.
    if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <0) {
    perror("Problem creando el Socket.");
    exit(2);
    }

    //Ceación del socket
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr= inet_addr(argv[1]);
    servaddr.sin_port = htons(SERV_PORT);

    //Conexión del cleinte al socket del servidor
    if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<0) {
    perror("Problema al intentar la conexión con el Servidor");
    exit(3);
    }

    while (fgets(sendline, MAXLINE, stdin) != NULL) {

    send(sockfd, sendline, strlen(sendline), 0);

    if (recv(sockfd, recvline, MAXLINE,0) == 0){

    perror("La conexión con el Servidor se terminó prematuramente.");
    exit(4);
    }
    printf("Cadena recibida del servidor: %s \n",recvline);

    }

    exit(0);
    }

    Lo compilas,.. gcc -g servidor -o servidor, y lo ejecutas ./servidor

  11. Andres Felipe /
    Usando Google Chrome Google Chrome 35.0.1916.114 en Mac OS X Mac OS X 10.9.0

    @Andres Felipe
    Perdón, le ejecutas así: ./servidor (dir. IP servidor)

  12. Gaspar Fernández / Post Author
    Usando Mozilla Firefox Mozilla Firefox 29.0 en Ubuntu Linux Ubuntu Linux

    @Andres Felipe
    Gracias por el aporte!! Los #include no salieron bien, pero bueno, nos los podemos imaginar 🙂

  13. Andres Felipe /
    Usando Google Chrome Google Chrome 35.0.1916.114 en Windows Windows 8

    @Gaspar Fernández

    hola, no había notado, aquí están.

    1
    2
    3
    4
    5
    6
    7
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <string.h>
    #include <arpa/inet.h>
    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 29.0 en Ubuntu Linux Ubuntu Linux

      Muchas gracias ! 🙂 ¡Qué apañado!

  14. Gaspar Fernández / Post Author
    Usando Mozilla Firefox Mozilla Firefox 29.0 en Ubuntu Linux Ubuntu Linux

    Por cierto, hace unos meses publiqué: https://poesiabinaria.net/2014/01/cliente-tcp-no-bloqueante-en-c-en-el-que-podemos-enviar-y-recibir-en-cualquier-momento/ un cliente tcp no bloqueante en C. Para que le echéis un ojo

  15. ingrid /
    Usando Google Chrome Google Chrome 24.0.1312.52 en Linux Linux

    Buen dia, excelente post, gracias por la ayuda.

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 30.0 en Linux Linux

      Gracias a ti Ingrid !

  16. Pablo Alvarez /
    Usando Internet Explorer Internet Explorer 10.0 en Windows Windows 8

    Excelente aporte, Felicidades!!

  17. wsn /
    Usando Debian IceWeasel Debian IceWeasel 38.7.1 en Linux Linux

    Muy buen aporte, gracias!

  18. pisculichi /
    Usando Mozilla Firefox Mozilla Firefox 45.0 en Ubuntu Linux Ubuntu Linux

    Excelente artículo!
    Que tarde encontre este blog, desde ahora en mi cliente rss siguiéndolos.

    Saludos

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 45.0 en Ubuntu Linux Ubuntu Linux

      Muchas gracias pisculichi. Si te gustó este, tal vez te guste: https://poesiabinaria.net/2016/02/creando-un-cliente-para-un-servicio-de-red-con-pocas-lineas-en-c/

      Es más, tengo en preparación varios posts de creación de servicios en C y C++

      Un cordial saludo

  19. anonimo24 /
    Usando Google Chrome Google Chrome 78.0.3904.97 en Windows Windows NT

    No se si alguien siga comentando pero, si quisiera que se pudiera abrir un archivo .txt y que el cliente lo lea en su terminal, como se podria hacer? URGEEE!!!!

  20. krunker /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    ¿Podría decirme el formato del código del cliente? Hice algunos pequeños cambios en el código para que use gethostbyname() para resolver dominios, pero todavía estoy confundido sobre cómo conectar clientes sin telnet.

Leave a Reply to anonimo24 Cancle Reply