cc31a Desarrollo de Software de Sistemas

Patricio Poblete (ppoblete@dcc.uchile.cl)

Programación en Redes

¿Cómo comunicar a dos programas que están corriendo (posiblemente) en máquinas distintas?

En Unix BSD apareció un mecanismo llamado sockets.

Tipos de comunicación

  1. connection-oriented:
    se establece un enlace entre los dos programas, el cual existe durante toda la "conversación". El enlace permite el envío de un flujo continuo de datos, preserva el orden de los mensajes y asegura la entrega confiable de los mensajes.

    En Internet esto se logra utilizando TCP, el cual provee además control de flujo.

  2. datagrams:
    envía paquetes de datos, sin que haya garantía sobre la confiabilidad de su entrega ni sobre el orden en que llegan.

    En Internet este servicio se entrega con UDP.

Para enviar un mensaje a un programa que corre en otra máquina es necesario especificar:

  1. La dirección IP de la máquina (p.ej. 192.80.24.83) o bien su nombre de dominio (p.ej. anakena.dcc.uchile.cl).

  2. El número del port en el cual el programa está escuchando.

Los ports 1-1024 están reservados para servicios "bien conocidos", como por ejemplo:


        80    http
        25    smtp
        21    ftp
        23    telnet

La dirección 127.0.0.1 (localhost) identifica a la máquina en dode estamos corriendo.

Los sockets proveen una interfaz uniforme sobre distintos medios de transporte. Por ejemplo,

Un esquema típico de comunicación es "cliente-servidor":


                browser <-----> httpd
        
                ftp <-----> ftpd

En Unix los servidores se suelen llamar daemons ("demonios").

Comunicación entre procesos (IPC) usando sockets

Estudiaremos primero lo que ocurre al lado del servidor.

Para recibir conexiones primero hay que crear un socket:


        #include <sys/types.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <netdb.h>
        
        int s=socket(int domain, int type, int protocol);
        
        /* s: descriptor del socket, similar a un fd (-1 si error)
           domain: AF_INET
           type: SOCK_STREAM (usaremos éste, permite encolar conexiones pendientes)
                 SOCK_DGRAM
           protocol: 0
        */

Una vez creado un socket hay que hacer un bind para darle una dirección en donde escuchar:


        status=bind(s, &sa, sizeof sa);
        
        /* status: 0 ==> OK, -1 ==> error
           sa: struct sockaddr
        */

El tipo struct sockaddr es la unión de diversos tipos, dependiendo del protocolo. Para Internet existe struct sockaddr_in, cuyo contenido es:


        struct sockaddr_in
          {
            sa_family_t    sin_family; /* address family: AF_INET */
            u_int16_t      sin_port;   /* port in network byte order */
            struct in_addr  sin_addr;  /* internet address */
          };
        /* Internet address. */
        struct in_addr
          {
            u_int32_t      s_addr;     /* address in network byte order */
          };

La estructura se rellena previamente así:


        struct sockaddr_in sa;
        struct hostent *hp;
        char myname[MAXHOSTNAME+1];
        
        bzero(&sa, sizeof(struct sockaddr_in));
        gethostname(myname, MAXHOSTNAME);
        hp=gethostbyname(myname);
        if(hp==NULL)
            return -1;
        sa.sin_family=AF_INET;
        sa.sin_port=htons(portnum);
          /* portnum lo elegimos nosotros, es de tipo ushort */
        bcopy(hp->h_addr_list[0], &sa.sin_addr.s_addr, hp->h_length);

La estructura hostent describe a un host y es de la forma


        struct hostent
          {
            char    *h_name;        /* official name of host */
            char    **h_aliases;    /* alias list */
            int     h_addrtype;     /* host address type */
            int     h_length;       /* length of address */
            char    **h_addr_list;  /* list of addresses */
         }
        #define h_addr  h_addr_list[0]  /* for backward compatibility */

Luego hay que comenzar a escuchar en ese port, indicando el número máximo de conexiones:


        listen(s, 3); /* máximo 3 conexiones */

Todo lo anterior se puede empaquetar en una función establish:

establish.c


        #include <sys/types.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <netdb.h>
        
        #define MAXHOSTNAME 255
        
        int establish(ushort portnum)
          {
            struct sockaddr_in sa;
            struct hostent *hp;
            char myname[MAXHOSTNAME+1];
            int s;
        
            bzero(&sa, sizeof(struct sockaddr_in));
            gethostname(myname, MAXHOSTNAME);
            hp=gethostbyname(myname);
            if(hp==NULL)
                return -1;
            sa.sin_family=AF_INET;
            sa.sin_port=htons(portnum);
            bcopy(hp->h_addr_list[0], &sa.sin_addr.s_addr, hp->h_length);
        
            if((s=socket(AF_INET, SOCK_STREAM, 0))<0)
                return -1;
            if(bind(s, (struct sockaddr *)&sa, sizeof sa)<0)
              {
                close(s);
                return -1;
              }
        
            listen(s, 3);
            return s;
          }

Una vez creado el socket hay que comenzar a aceptar conexiones;

get_connection.c


        #include <sys/types.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        
        int get_connection(int s)
          {
            struct sockaddr_in isa;
            int i;
            int t;
        
            i=sizeof(isa);
            getsockname(s, (struct sockaddr *)&isa, &i);
            if((t=accept(s, (struct sockaddr *)&isa, &i))<0)
                return -1;
        
            return t; /* nuevo socket creado para esta conexión,
                         se usa como un fd */
          }

Para atender varias conexiones sin que el servidor quede bloqueado se pueden crear procesos ad hoc:

servidor.c


        #include <stdio.h>
        #include <errno.h>
        #include <signal.h>
        #include <sys/types.h>
        #include <sys/socket.h>
        #include <wait.h>
        #include <netinet/in.h>
        #include <netdb.h>
        
        #define PORTNUM 50000 /* arbitrario */
        
        void recoge_hijos()
          {
            int wstatus;
        
            (void)wait(&wstatus);
          }
        
        main()
          {
            int s, t;
        
            if((s=establish(PORTNUM))<0)
              {
                perror("establish");
                exit(1);
              }
        
            signal(SIGCHLD, recoge_hijos); /* para evitar zombies */
        
            for(;;)
              {
                if((t=get_connection(s))<0)
                  {
                    if(errno==EINTR)
                        continue; /* OK, puede ocurrir en accept */
                    perror("accept");
                    exit(1);
                  }
        
                if(fork()==0)
                  {
                    /* hijo */
                    /* usar socket t aquí */
                  }
                else
                  {
                    /* padre */
                    close(t);
                  }
              }
          }

Veamos ahora lo que ocurre al lado del cliente:

call_socket.c


        #include <sys/types.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <netdb.h>
        #include <errno.h>
        
        int call_socket(char *hostname, ushort portnum)
          {
            struct sockaddr_in sa;
            struct hostent *hp;
            int a, s;
        
            if((hp=gethostbyname(hostname))==NULL)
              {
                errno=ECONNREFUSED;
                return -1;
              }
            bzero(&sa, sizeof(sa));
            bcopy(hp->h_addr_list[0], &sa.sin_addr.s_addr, hp->h_length);
            sa.sin_family=AF_INET;
            sa.sin_port=htons(portnum);
        
            if((s=socket(AF_INET, SOCK_STREAM, 0))<0)
                return -1;
        
            if(connect(s, (struct sockaddr *)&sa, sizeof(sa))<0)
              {
                close(s);
                return -1;
              }
        
            return s;
          }

Entrada/Salida en un socket

Los sockets se usan como si fueran fds abiertos. AL terminar de usarlos hay que hacer close(s).

Hay que tener cuidado de que es posible que los datos no sean transmitidos todos de una sola vez.

read_data.c


        int read_data(int s, char *buf, int n)
        /* lee n bytes hacia buf desde el socket s */
          {
            int bcount, br;
        
            bcount=0;
        
            while(bcount<n)
              {
                if((br=read(s, buf, n-bcount))>0)
                  {
                    bcount+=br;
                    buf+=br;
                  }
              }
            if(br<0)
                return -1;
          }

Ejercicio: Escribir write_data.

Transmisión de datos a través de la red

La transmisión de datos de tipo char no presenta problemas.

En cambio, es posible que los datos de tipo int, short, etc. lleguen en forma incorrecta. El problema es que hay distintas arquitecturas de computadores que almacenan los bytes dentro de una "palabra" en distinto orden.

Para evitar confusiones en la transmisión, se adopta un orden canónico para los datos que viajan por la red. Este orden se llama "network order".

Para transmitir:


        #include <netinet/in.h>
        
        unsigned long int a, b;
        
        b=htonl(a); /* a está en "host order", b está en "network order" */
        write_data(s, &b, sizeof b);

Para recibir:


        unsigned long int a, b;
        
        read_data(s, &b, sizeof b);
        a=ntohl(b);

También existen funciones para shorts (htons, ntohs).

Una biblioteca de "simple sockets"

Para evitar algunas de las complicaciones del uso de sockets podemos usar la siguiente biblioteca:

jsocket.h


        #include <stdio.h>
        
        int j_socket();
        int j_bind(int, int);
        int j_accept(int);
        int j_connect(int, char *, int);

jsocket.c


        #include <sys/types.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        
        #include <stdio.h>
        #include <sys/wait.h>
        
        #include <netdb.h>       
        
        #ifdef BSD
        #include <strings.h> 
        #else
        #define bcopy(s1,s2,n) memcpy(s2,s1,n)
        #endif
        
        #include "jsocket.h"
        
        
        /*
         * Retorna un socket para conexion
         */
        
        int j_socket()
        {
            return(socket(AF_INET, SOCK_STREAM, 0));
        }
        
        static struct sockaddr_in portname;
        
        /*
         * Pone un "nombre" (port) a un socket
         * y lo prepara para recibir conexiones
         * retorna 0 si OK, -1 si no
         */
        
        int j_bind(s, port)
        int s;
        int port;
        {
                /* ponemos el nombre */
                portname.sin_port = htons(port);
                portname.sin_family = AF_INET;
                portname.sin_addr.s_addr = INADDR_ANY;
        
                /* lo asociamos a el socket */
                if( bind(s, (struct sockaddr *) &portname, sizeof portname) != 0)
                        return(-1);
        
                listen(s, 5);
        	return(0);
        }
        
        /*
         * Acepta una conexion pendiente o se bloquea esperando una
         * retorna un fd si OK, -1 si no
         */
        int j_accept(s)
        int s;
        {
        	struct sockaddr_in from;
        	int size = 0;
        
        	return( accept(s, (struct sockaddr *) &from, &size) );
        }
        
        /*
         * Se conecta con un port conocido
         * retorna 0 si OK, -1 si no
         */
        int j_connect(s, host, port)
        int s;
        char *host;
        int port;
        {
                struct hostent *hp;
        	int i;
        
                /* Traducir nombre a direccion IP */
        	hp = gethostbyname(host);
        	if( hp == NULL )
        	    return(-1);
        	
                /* Especificar port del servidor */
        	portname.sin_port = htons(port);
                portname.sin_family = AF_INET;
        
                /* Trato de conectarme con todas las direcciones IP del servidor */ 
        	for(i=0; hp->h_addr_list[i] != NULL; i++) {
        	    bcopy( hp->h_addr_list[i], &portname.sin_addr.s_addr, hp->h_length);
        	    
        	    if(connect(s, (struct sockaddr *)&portname, sizeof portname) == 0)
                        return(0);
                }                                                 
        
                /* No logre' conectarme */
                return(-1);
        }

Resumen de operaciones

Modelo de un servidor

jservidor.c


        /* responde enviando 1 byte hacia el cliente */
        
        #include "jsocket.h"
        
        main()
          {
            int s;
            int fd;
            char c=0;
        
            s=j_socket();
            if(s<0)
              {
                perror("socket");
                exit(-1);
              }
            if(j_bind(s, 7001)<0)
              {
                perror("bind");
                exit(-1);
              }
        
            for(;;)
              {
                if((fd=j_accept(s))<0)
                  {
                    perror("accept");
                    exit(-1);
                  }
                write(fd, &c, 1);
                ++c;
                close(fd);
              }
          }

Modelo de un cliente

jcliente.c


        /* lee un byte desde el servidor */
        
        #include <stdio.h>
        #include "jsocket.h"
        
        main(int argc, char *argv[])
          {
            int s;
            char c;
        
            if(argc!=2)
              {
                fprintf(stderr, "Use: %s host\n", argv[0]);
                exit(1);
              }
        
            s=j_socket();
            if(s<0)
              {
                perror("socket");
                exit(1);
              }
            if(j_connect(s, argv[1], 7001)<0)
              {
                perror("connect");
                exit(1);
              }
        
            if(read(s, &c, 1)!=1)
              {
                perror("read");
                exit(1);
              }
            printf("%d\n", c);
            close(s);
            exit(0);
          }

Un "super demonio" (inetd)

Este es un servicio que se configura con una tabla de la forma

        port   path
        port   path
        port   path
        ...    ...
Cada vez que se recibe un pedido por alguno de los ports indicados en la tabla, se echa a andar al programa identificado por el path respectivo, dándole como entrada y salida estándar al socket conectado al cliente.

La implementación de inetd requiere que el programa pueda estar escuchando desde muchos ports al mismo tiempo. Eso no se puede hacer con un solo accept, porque esa operación funciona con un solo port. En lugar de eso, utilizaremos select.


        n=select(numfds, readfds, writefds, excepfds, timeout);

Esta instrucción vigila si hay algo que leer en algún fd indicado en las readfds, si se puede escribir en alguna de las writefds, etc. Además actualiza las fds para indicar cuales fueron detectadas y retorna el número de fds detectadas.

Los conjuntos de fds son representados mediante bitmaps (de tipo fd_set *). Para manipular bits se proveen las funciones


        FD_CLR(fd, set);  /* apaga bit correspondiente a fd en set */
        
        FD_SET(fd, set);  /* enciende bit correspondiente a fd en set */
        
        FD_ISSET(fd, set); /* pregunta si está encendido */
        
        FD_ZERO(set); /* apaga todos los bits */

Los argumentos que no se usan en select se pasan como NULL.

Formato general de inetd

Suponemos que el contenido del archivo de configuración se lee hacia un arreglo de structs.

inetd.c


        #include <stdio.h>
        #include <sys/time.h>
        #include <sys/types.h>
        #include <unistd.h>
        #include <signal.h>
        #include "jsocket.h"
        
        #define MAXSERVERS 20
        
        struct
          {
            int port;
            char *path;
          } servers[MAXSERVERS];
        int nservers; /* cuántos servidores hay realmente definidos */
        
        void recoge_hijos()
          {
            int val;
        
            wait(&val);
          }
        
        main()
          {
            int sock[MAXSERVERS];
            int i;
            fd_set mask;
            int s;
        
            /* aquí se deben inicializar servers[] y nservers */
        
            signal(SIGCHLD, recoge_hijos);
        
            for(i=0; i<nservers; ++i)
              {
                sock[i]=j_socket();
                j_bind(sock[i], servers[i].port);
              }
        
            /* quedamos a la espera de conexiones */
            for(;;)
              {
                FD_ZERO(&mask);
                for(i=0; i<nservers; ++i)
                    FD_SET(sock[i], &mask);
                select(32, &mask, NULL, NULL, NULL);
                for(i=0; i<nservers; ++i)
                  {
                    if(FD_ISSET(sock[i], &mask))
                      {
                        s=j_accept(sock[i]);
                        if(fork()==0)
                          { /* hijo */
        	            /* redirigimos I/O */
                            close(0); /* STDIN_FILENO */
                            close(1); /* STDOUT_FILENO */
                            dup(s);
                            dup(s);
                            close(s);
                            execl(servers[i].path, servers[i].path, NULL);
                            exit(-1); /* si no funciona el exec */
                          }
                        /* padre */
                        close(s);
                      }
                  }
              }
          }