Patricio Poblete (ppoblete@dcc.uchile.cl) |
¿Cómo comunicar a dos programas que están corriendo (posiblemente) en máquinas distintas?
En Unix BSD apareció un mecanismo llamado sockets.
En Internet esto se logra utilizando TCP, el cual provee además control de flujo.
En Internet este servicio se entrega con UDP.
Para enviar un mensaje a un programa que corre en otra máquina es necesario especificar:
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").
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; }
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.
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).
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); }
s=j_socket();
status=j_bind(s, port); /* 0 ==> OK, -1 ==> error */
t=j_accept(s); /* -1 si error */
status=j_connect(s, host, port); /* 0 ==> OK, -1 ==> error */
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); } }
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); }
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.
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); } } } }