Guía Beej de programación en redes

Anterior


Siguiente


5. Modelo Cliente-Servidor

Amigo, este es un mundo cliente-servidor. Casi cualquier cosa en la red tiene que ver con procesos clientes que dialogan con procesos servidores y viceversa. Consideremos telnet, por ejemplo. Cuando conectas al puerto 23 de una máquina remota mediante telnet (el cliente) un programa de aquella máquina (llamado telnetd, el servidor) despierta a la vida. Gestiona la conexión telnet entrante, te presenta una pantalla de login , etc.

Figura 2. Interacción Cliente-Servidor.

[Client-Server Interaction Diagram]

El intercambio de información entre cliente y servidor se resume en la Figura 2 .

Observa que el par cliente-servidor pueden hablar SOCK_STREAM , SOCK_DGRAM o cualquier otra cosa (siempre y cuando los dos hablen lo mismo). Algunos buenos ejemplos de parejas cliente-servidor son telnet/telnetd , ftp/ftpd, o bootp/bootpd. Cada vez que usas ftp, hay un programa remoto, ftpd, que te sirve.

Con frecuencia, solamente habra un servidor en una máquina determinada, que atenderá a múltiples clientes usando fork(). El funcionamiento básico es: el servidor espera una conexión, la acepta (accept()) y usa fork() para obtener un proceso hijo que la atienda. Eso es lo que hace nuestro servidor de ejemplo en la siguiente sección.

5.1. Un servidor sencillo

Todo lo que hace este servidor es enviar la cadena " Hello, World!\n" sobre una conexión de flujo. Todo lo que necesitas para probar este servidor es ejecutarlo en una ventana y atacarlo con telnet desde otra con:

    $ telnet remotehostname 3490

donde remotehostname es el nombre de la máquina donde estas ejecutando.

El código servidor : (Nota: una barra invertida al final de una línea indica que esa línea se continúa en la siguiente.)



    /*
    ** server.c -- Ejemplo de servidor de sockets de flujo
    */

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

    #define MYPORT 3490    // Puerto al que conectarán los usuarios

    #define BACKLOG 10     // Cuántas conexiones pendientes se mantienen en cola

    void sigchld_handler(int s)
    {
        while(wait(NULL) > 0);
    }

    int main(void)
    {
        int sockfd, new_fd;  // Escuchar sobre sock_fd, nuevas conexiones sobre new_fd
        struct sockaddr_in my_addr;    // información sobre mi dirección
        struct sockaddr_in their_addr; // información sobre la dirección del cliente
        int sin_size;
        struct sigaction sa;
        int yes=1;

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

        if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }
        
        my_addr.sin_family = AF_INET;         // Ordenación de bytes de la máquina
        my_addr.sin_port = htons(MYPORT);     // short, Ordenación de bytes de la red
        my_addr.sin_addr.s_addr = INADDR_ANY; // Rellenar con mi dirección IP
        memset(&(my_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura

        if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
                                                                       == -1) {
            perror("bind");
            exit(1);
        }

        if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
        }

        sa.sa_handler = sigchld_handler; // Eliminar procesos muertos
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        while(1) {  // main accept() loop
            sin_size = sizeof(struct sockaddr_in);
            if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
                                                           &sin_size)) == -1) {
                perror("accept");
                continue;
            }
            printf("server: got connection from %s\n",
                                               inet_ntoa(their_addr.sin_addr));
            if (!fork()) { // Este es el proceso hijo
                close(sockfd); // El hijo no necesita este descriptor
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // El proceso padre no lo necesita
        }

        return 0;
    } 

Por si sientes curiosidad, tengo todo el código en una gran función main() porque me parece más claro. Puedes partirlo en funciones más pequeñas si eso te hace sentir mejor.

(Además, esto del sigaction() podría ser nuevo para ti--es normal. El código que hay ahí se encarga de limpiar los procesos zombis que pueden aparecer cunado los procesos hijos finalizan. Si creas muchos procesos zombis y no los eliminas, tu administrador de sistema se mosqueará)

Puedes interactuar con este servidor usando el cliente de la siguiente sección.

5.2. Un cliente sencillo

Este tío es incluso más sencillo que el servidor. Todo lo que hace es conectar al puerto 3490 de la máquina que indicas en la línea de comandos y obtiene la cadena que el servidor envía.

El código cliente :



    /*
    ** client.c -- Ejemplo de cliente de sockets de flujo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>

    #define PORT 3490 // puerto al que vamos a conectar 

    #define MAXDATASIZE 100 // máximo número de bytes que se pueden leer de una vez 

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;  
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // información de la dirección de destino 

        if (argc != 2) {
            fprintf(stderr,"usage: client hostname\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // obtener información de máquina 
            perror("gethostbyname");
            exit(1);
        }

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

        their_addr.sin_family = AF_INET;    // Ordenación de bytes de la máquina 
        their_addr.sin_port = htons(PORT);  // short, Ordenación de bytes de la red 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), 8);  // poner a cero el resto de la estructura 

        if (connect(sockfd, (struct sockaddr *)&their_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("connect");
            exit(1);
        }

        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
            perror("recv");
            exit(1);
        }

        buf[numbytes] = '\0';

        printf("Received: %s",buf);

        close(sockfd);

        return 0;
    } 

Observa que si no ejecutas el servidor antes de llamar al cliente, connect() devuelve "Connection refused" (Conexión rechazada). Muy útil.

5.3. Sockets de datagramas

En realidad no hay demasiado que contar aquí, así que sólo presentaré un par de programas de ejemplo: talker.c y listener.c.

listener se sienta a esperar en la máquina hasta que llega un paquete al puerto 4950. talker envía un paquete a ese puerto en la máquina indicada que contiene lo que el usuario haya escrito en la línea de comandos.

Este es el  código fuente de listener.c :



    /*
    ** listener.c -- Ejemplo de servidor de sockets de datagramas
    */

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

    #define MYPORT 4950    // puerto al que conectarán los clientes

    #define MAXBUFLEN 100

    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // información sobre mi dirección
        struct sockaddr_in their_addr; // información sobre la dirección del cliente
        int addr_len, numbytes;
        char buf[MAXBUFLEN];

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

        my_addr.sin_family = AF_INET;         // Ordenación de bytes de máquina
        my_addr.sin_port = htons(MYPORT);     // short, Ordenación de bytes de la red
        my_addr.sin_addr.s_addr = INADDR_ANY; // rellenar con mi dirección IP
        memset(&(my_addr.sin_zero), '\0', 8); // poner a cero el resto de la estructura

        if (bind(sockfd, (struct sockaddr *)&my_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0,
                           (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }

        printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
        printf("packet is %d bytes long\n",numbytes);
        buf[numbytes] = '\0';
        printf("packet contains \"%s\"\n",buf);

        close(sockfd);

        return 0;
    } 

Observa que en nuestra llamada a socket() finalmente estamos usando SOCK_DGRAM. Observa también que no hay necesidad de escuchar (listen()) o aceptar (accept()). ¡Esa es una de las ventajas de usar sockets de datagramas sin conexión!

A continuación el  código fuente de talker.c :



    /*
    ** talker.c -- ejemplo de cliente de datagramas
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <netdb.h>

    #define MYPORT 4950    // puerto donde vamos a conectarnos

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // información sobre la dirección del servidor
        struct hostent *he;
        int numbytes;

        if (argc != 3) {
            fprintf(stderr,"usage: talker hostname message\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // obtener información de máquina
            perror("gethostbyname");
            exit(1);
        }

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

        their_addr.sin_family = AF_INET;     // Ordenación de bytes de máquina
        their_addr.sin_port = htons(MYPORT); // short, Ordenación de bytes de la red
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8); // poner a cero el resto de la estructura

        if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
            perror("sendto");
            exit(1);
        }

        printf("sent %d bytes to %s\n", numbytes,
                                               inet_ntoa(their_addr.sin_addr));

        close(sockfd);

        return 0;
    } 

¡Y esto es todo! Ejecuta listener en una máquina y luego llama a talker en otra. ¡Observa cómo se comunican! Watch them communicate! ¡Disfruta de toda la excitación de la familia nuclear entera!

Excepto un pequeño detalle que he mencionado muchas veces con anterioridad: sockets de datagramas con conexión. Tengo que hablar de ellos aquí, puesto que estamos en la sección de datagramas del documento. Supongamos que talker llama a connect() e indica la dirección de listener. A partir de ese momento, talker solamente puede enviar a y recibir de la dirección especificada por connect(). Por ese motivo no tienes que usar sendto() y recvfrom(); tienes que usar simplemente send() y recv().


Anterior

Inicio

Siguiente

Llamadas al sistema


Técnicas moderadmente avanzadas