Professional Documents
Culture Documents
Historial de Revisiones
Versin de la revisin: 1.0.0
Versin Inicial
Revisado por: beej Revisado por: beej Revisado por: beej Revisado por: beej
Convertido a DocBook XML, correcciones, aadidos. Corregidas algunas erratas, error de sintaxis en client.c, se aaden algunas cosas a la seccin de Preguntas ms comunes.
Pgina 1
Tabla de Contenidos
1. Introduccin 1.1. Destinatarios 1.2. Plataforma y compilador 1.3. Sitio web oficial 1.4. Nota para programadores Solaris/SunOS 1.5. Nota para programadores Windows 1.6. Poltica de respuesta a correos electrnicos 1.7. Replicacin 1.8. Nota para traductores 1.9. Copyright y distribucin 2. Qu es un socket? 2.1. Dos tipos de Sockets de Internet 2.2. Tonteras de bajo nivel y teora de redes 3. structs y manipulacin de datos 3.1. Convierte a valores nativos! 3.2. Direcciones IP y como tratarlas 4. Llamadas al sistema 4.1. socket()--Consigue el descriptor de fichero! 4.2. bind()--En qu puerto estoy? 4.3. connect()--Eh, t! 4.4. listen()--Por favor que alguien me llame 4.5. accept()--"Gracias por llamar al puerto 3490." 4.6. send() y recv()--Hblame, baby! 4.7. sendto() y recvfrom()--Hblame al estilo DGRAM 4.8. close() y shutdown()--Fuera de mi vista! 4.9. getpeername()--Quin eres t? 4.10. gethostname()--Quin soy yo? 4.11. DNS--T dices "whitehouse.gov", Yo digo "198.137.240.92" 5. Modelo Cliente-Servidor 5.1. Un servidor sencillo 5.2. Un cliente sencillo 5.3. Sockets de datagramas 6. Tcnicas moderadamente avanzadas 6.1. Bloqueo 6.2. select()--Multiplexado de E/S sncrona 6.3. Gestin de envos parciales con send() 6.4.Consecuencias de la encapsulacin de datos 7. Referencias adicionales 7.1. Pginas del manual (man) 7.2. Libros 7.3. Referencias en la web 7.4. RFCs 8. Preguntas ms comunes 9. Declinacin de responsabilidad y solicitud de ayuda
Pgina 2
1. Introduccin
Eh!, la programacin de sockets te quita el sueo? Todo esto es demasiado complicado para desentraarlo slo con las pginas de man? Quieres hacer buenos programas para Internet, pero no tienes tiempo de pelearte con montones de structs para tratar de adivinar si tienes que ejecutar bind() antes que connect() , etc., etc. Bueno, sabes qu?, ya me he ocupado de todo ese molesto asunto y estoy deseando compartir la informacin con todo el mundo. Has venido al lugar adecuado. Este documento debera bastar para que un programador en C medianamente competente empiece a trabajar en un entorno de redes.
1.1. Destinatarios
Este documento se ha escrito a modo de tutorial, y no como una gua de referencia. Probablemente, es de mxima utilidad para personas que estn empezando a programar con sockets y buscan donde apoyarse. No es, en absoluto, la gua completa de programacin de sockets. Sin embargo, con suerte ser suficiente para que esas pginas de man empiecen a cobrar sentido... :-)
Si todava tienes errores puedes probar a aadir " -lxnet" al final de la lnea de comandos. No s qu es lo que hace exactamente, pero algunas personas parecen necesitarlo para compilar correctamente. Otro lugar donde podras encontrar problemas es en la llamada a setsockopt() . El prototipo
Pgina 3
teclea esto:
char yes='1';
Como no tengo una mquina Sun no he probado ninguna de la informacin anterior--slamente es lo que la gente me ha comentado por correo electrnico.
Espera! Tambin tienes que ejecutar WSAStartup() antes de hacer nada con la biblioteca de sockets. El cdigo para hacerlo es algo as como:
#include <winsock.h> { WSADATA wsaData; // Si esto no funciona //WSAData wsaData; // prueba esto en su lugar if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { fprintf(stderr, "WSAStartup failed.\n"); exit(1); }
Tambin tienes que decirle a tu compilador que enlace la biblioteca Winsock, que normalmente se llama wsock32.lib o winsock32.lib o algo as. Con VC++, esto puede hacerse con el men Project , bajo Settings... Haz click en la pestaa Link y busca el cuadro titulado "Object/library modules". Aade "wsock32.lib " a esa lista. Al menos eso es lo que he odo. Por ltimo, necesitas ejecutar WSACleanup() cuando hayas terminado con la biblioteca de sockets. Consulta la ayuda en lnea para los detalles. Una vez hecho esto, el resto de ejemplos de este tutorial deberan funcionar con algunas pocas excepciones. En primer lugar, no puedes usar close() para cerrar un socket--en su lugar, tienes que usar closesocket() . Ademas, select() slo funciona con descriptores de sockets pero no con descriptores de ficheros (como 0 para stdin). Tambin hay una clase socket que puedes utilizar, Csocket. Consulta la ayuda de tu compilador para ms informacin
Pgina 4
Para ms informacin acerca de Winsock lee el FAQ de Winsock . Finalmente, he odo que Windows no tiene la llamada al sistema fork() que, desgraciadamente, uso en algunos de mis ejemplos. Quizs debas enlazar tambin una biblioteca POSIX o algo para que funcione, o puedes usar CreateProcess() en su lugar. fork() no tiene argumentos, y CreateProcess() tiene alrededor de 48 billones de argumentos. Si no ests dispuesto a lidiar con todos ellos, CreateThread() es un poco ms fcil de digerir... Desgraciadamente, una explicacin en profundidad de las tcnicas multihebra (multithreading) va mucho ms all del alcance de este documento. No puedo hablar de todo, sabes!
1.7. Replicacin
Es una excelente idea si quieres replicar este sitio, bien sea en pblico o en privado. Si replicas este sitio en pblico y quieres que aada un enlace desde la pgina principal escrbeme a: < beej@piratehaven.org >.
Pgina 5
Pgina 6
2. Qu es un socket?
Continuamente oyes hablar de los "sockets", y tal vez te ests preguntando qu es lo que son exactamente. Bien, esto es lo que son: una forma de comunicarse con otros programas usando descriptores de fichero estndar de Unix. Qu? Vale--puede que hayas odo a algn hacker de Unix decir: "Hey, todo en Unix es un fichero!" A lo que esta persona se estaba refiriendo es al hecho de que cuando los programas de Unix hacen cualquier tipo de E/S, lo hacen escribiendo o leyendo un descriptor de fichero. Un descriptor de fichero no es ms que un entero asociado a un archivo abierto. Pero (y aqu est el quid de la cuestin) ese fichero puede ser una conexin de red, una cola FIFO, un tubo [pipe], un terminal, un fichero real de disco, o cualquier otra cosa. Todo en Unix es un fichero! Por eso, si quieres comunicarte con otro programa a travs de Internet vas a tener que hacerlo con un descriptor de fichero, es mejor que lo creas. "De dnde saco yo este descriptor de fichero para comunicar a travs de la red, seor sabelotodo?" Esta es probablemente la ltima pregunta en la que piensas ahora, pero voy a contestarla de todas maneras: usas la llamada al sistema socket(). Te devuelve un descriptor de fichero y t te comunicas con l usando las llamadas al sistema especializadas send() y recv() ( man send , man recv ). "Pero, oye!" puede que exclames ahora. "Si es un descriptor de fichero, por qu, en el nombre de Neptuno, no puedo usar las llamadas normales read() y write() para comunicarme a travs de un socket ." La respuesta corta es, "S que puedes!" La respuesta larga es, "Puedes usarlas, pero send() y recv() te ofrecen mucho ms control sobre la transmisin de datos." Y ahora qu? Qu tal esto: hay muchos tipos de sockets. Existen las direcciones de Internet DARPA (sockets de internet), rutas sobre nodos locales (sockets de Unix), direcciones X.25 del CCITT ( sockets stos de X.25 que puedes ignorar perfectamente) y probablemente muchos ms en funcin de la modalidad de Unix que ests ejecutando. Este documento se ocupa nicamente de los primeros: los sockets de Internet.
Si envas dos tems a travs del socket en el orden "1, 2" llegarn al otro extremo en el orden "1, 2", y llegarn sin errores. Cualquier error que encuentres es producto de tu extraviada mente, y no lo vamos a discutir aqu. Qu aplicacin tienen los sockets de flujo? Bueno, quizs has odo hablar del programa telnet. S? telnet usa sockets de flujo. Todos los carcteres que tecleas tienen que llegar en el mismo orden en que t los tecleas, no? Tambin los navegadores, que usan el protocolo HTTP , usan sockets de flujo para obtener las pginas. De hecho, si haces telnet a un sitio de la web sobre el puerto 80, y escribes " GET / ", recibirs como respuesta el cdigo HTML. Cmo consiguen los sockets de flujo este alto nivel de calidad en la transmisin de datos? Usan un protocolo llamado "Protocolo de Control de Transmisin", ms conocido como "TCP" (consulta el RFC-793 para informacin extremadamente detallada acerca de TCP). TCP asegura que tu informacin llega secuencialmente y sin errores. Puede que hayas odo antes "TCP" como parte del acrnimo " TCP/IP ", donde "IP" significa "Protocolo de Internet" (consulta el RFC-791 .) IP se encarga bsicamente del encaminamiento a travs de Internet y en general no es responsable de la integridad de los datos. Estupendo. Qu hay de los sockets de datagramas? Por qu se les llama sockets sin conexin? De qu va esto, en definitiva, y por qu no son fiables? Bueno, estos son los hechos: si envas un datagrama, puede que llegue. Puede que llegue fuera de secuencia. Si llega, los datos que contiene el paquete no tendrn errores. Los sockets de datagramas tambin usan IP para el encaminamiento, pero no usan TCP; usan el "Protocolo de Datagramas de Usuario" o "UDP" (consulta el RFC-768 .) Por qu son sin conexin? Bueno, bsicamente porque no tienes que mantener una conexin abierta como haras con los sockets de flujo. Simplemente montas un paquete, le metes una cabecera IP con la informacin de destino y lo envas. No se necesita conexin. Generalmente se usan para transferencias de informacin por paquetes. Aplicaciones que usan este tipo de sockets son, por ejemplo, tftp y bootp. "Basta!" puede que grites. "Cmo pueden siquiera funcionar estos programas si los datagramas podran llegar a perderse?". Bien, mi amigo humano, cada uno tiene su propio protocolo encima de UDP. Por ejemplo, el protocolo tftp establece que, para cada paquete enviado, el receptor tiene que devolver un paquete que diga, "Lo tengo!" (un paquete "ACK"). Si el emisor del paquete original no obtiene ninguna respuesta en, vamos a decir, cinco segundos, retransmitir el paquete hasta que finalmente reciba un ACK . Este procedimiento de confirmaciones es muy importante si se implementan aplicaciones basadas en SOCK_DGRAM.
Pgina 8
Eh tos, es hora de aprender algo sobre Encapsulacin de datos ! Esto es muy, muy importante. Tan importante que podras aprenderlo si te matricularas en el curso de redes aqu, en el estado de Chico ;-). Bsicamente consiste en esto: un paquete de datos nace, el paquete se envuelve (se "encapsula") con una cabecera (y raramente con un pie) por el primer protocolo (por ejemplo el protocolo TFTP), entonces todo ello (cabecera de TFTP incluida) se encapsula otra vez por el siguiente protocolo (por ejemplo UDP), y otra vez por el siguiente (IP) y otra vez por el protocolo final o nivel (fsico) del hardware (por ejemplo, Ethernet) Cuando otro ordenador recibe el paquete, el hardware retira la cabecera Ethernet, el ncleo [kernel] retira las cabeceras IP y UDP, el programa TFTP retira la cabecera TFTP , y finalmente obtiene los datos. Ahora puedo finalmente hablar del conocido Modelo de redes en niveles [Layered Network Model]. Este modelo de red describe un sistema de funcionalidades de red que tiene muchas ventajas sobre otros modelos. Por ejemplo, puedes escribir programas de sockets sin preocuparte de cmo los datos se transmiten fsicamente (serie, thin ethernet, AUI, lo que sea) porque los programas en los niveles ms bajos se ocupan de eso por ti. El hardware y la topologa real de la red son transparentes para el programador de sockets. Sin ms prembulos, te presento los niveles del modelo completo. Recuerda esto para tus exmenes de redes:
Aplicacin
Presentacin
Sesin
Transporte
Red
Enlace de datos
Fsico
El nivel fsico es el hardware (serie, Ethernet, etc.) El nivel de aplicacin est tan lejos del nivel fsico como puedas imaginar--es el lugar donde los usarios interactan con la red. Sin embargo, este modelo es tan general que si quisieras podras usarlo como una gua para reparar coches. Un modelo de niveles ms consistente con Unix podra ser:
En este momento, probablemente puedes ver como esos niveles se corresponden con la encapsulacin de los datos originales. Ves cunto trabajo se necesita para construir un solo paquete? Dios! Y tienes que teclear la cabecera de los paquetes t mismo, usando "cat"! Slo bromeaba. Todo lo que tienes que hacer con los sockets de flujo es usar send() para enviar tus datos. Para los sockets de datagramas tienes que encapsular el paquete segn el mtodo de tu eleccin y enviarlo usando sendto(). El ncleo implementa los niveles de Internet y de Transporte por ti, y el hardware el nivel de acceso a la red. Ah, tecnologa moderna. As finaliza nuestra breve incursin en la teora de redes. Ah, olvidaba contarte todo lo que quera decir acerca del encaminamiento: nada! Exacto, no voy a hablar de l en absoluto. el encaminador retira la informacin de la cabecera IP, consulta su tabla de encaminamiento, bla, bla, bla. Consulta el RFC sobre IP si de verdad te importa. Si nunca aprendes nada de eso probablemente sobrevivirs.
Pgina 10
Nada ms que un int. A partir de aqu las cosas se complican, as que letelo todo y ten paciencia conmigo. Debes saber, para empezar, que existen dos formas distintas de ordenacin de bytes (a veces se les llama "octetos"): primero el byte ms significativo, o primero el byte menos significativo. A la primera forma se la llama "Ordenacin de bytes de la red" [Network Byte Order]. Algunos ordenadores almacenan internamente los nmeros segn la Ordenacin de bytes de la red, mientras que otros no. Cuando diga que algo tiene que seguir la Ordenacin de bytes de la red, tendrs que llamar a alguna funcin (como por ejemplo htons() ) para realizar la conversin desde la "Ordenacin de bytes de mquina" [Host Byte Order]. Si no digo "Ordenacin de bytes de la red", entonces puedes dejarlo en la Ordenacin de bytes de mquina. (Para los que sientan curiosidad, la "Ordenacin de bytes de la red" tambin se conoce como ordenacin Big-Endian.) Mi Primer StructTM--struct sockaddr. Esta estructura mantiene informacin de direcciones de socket para diversos tipos de sockets:
struct sockaddr { unsigned short char }; sa_family; sa_data[14]; // familia de direcciones, AF_xxx // 14 bytes de la direccin del protocolo
sa_family admite varios valores, pero ser AF_INET para todo lo que hagamos en este documento. sa_data contiene una direccin y nmero de puerto de destino para el socket. Esto resulta bastante farrogoso, porque no resulta nada agradable tener que empaquetar a mano una direccin y un nmero de puerto dentro de sa_data . Por eso, para manejar struct sockaddr ms cmodamente, los programadores han creado una estructura paralela: struct sockaddr_in ("in" por "Internet".)
struct sockaddr_in { short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; original de struct sockaddr }; // // // // familia de direcciones, AF_INET Nmero de puerto Direccin de Internet Relleno para preservar el tamao
Esta estructura hace ms sencillo referirse a los elementos de la direccin de socket. Observa que sin_zero (que se incluye para que la nueva estructura tenga el mismo tamao que un struct sockaddr ) debe rellenarse todo a ceros usando la funcin memset(). Adems, y
Pgina 11
es este un detalle importante, un puntero a struct sockaddr_in puede forzarse [cast ] a un puntero a struct sockaddr y viceversa. As, aunque socket() exige un struct sockaddr* , puedes usar en su lugar un struct sockaddr_in y forzarlo en el ltimo momento. Observa tambin que el sin_family se corresponde con el sa_family de struct sockaddr y debe asignrsele siempre el valor " AF_INET". Por ltimo, sin_port y sin_addr tienen que seguir la Ordenacin de bytes de la red. "Pero", podras preguntar ahora, "cmo puede toda la estructura struct in_addr sin_addr, seguir la Ordenacin de bytes de la red?" Esta pregunta requiere un cuidadoso examen de la estructura struct in_addr , una de las peores uniones que existen:
// Direccin de Internet (una estructura por herencia histrica) struct in_addr { unsigned long s_addr; // Esto es un long de 32 bits, 4 bytes };
Bueno, en el pasado era una union, pero esos tiempos parecen haber pasado. Celebro que as sea. De modo que, si has declarado la variable ina asignndole el tipo struct sockaddr_in, entonces ina.sin_addr.s_addr se refiere a la direccin IP de 4 bytes (segn la Ordenacin de bytes de la red). Observa que, aunque tu sistema use todava esa aberrante union para el struct in_addr, sigue siendo posible referirse a la direccin IP de 4 bytes de la misma manera en que yo lo hice antes (debido a algunos #defineS).
Pgina 12
Ahora, puedes creer que le ests cogiendo el truco a esto. Podras pensar, "Qu pasa si tengo que cambiar la Ordenacin de bytes de un char ?", entonces podras pensar, "Bueno, en realidad no importa". Tambin podras pensar que, puesto que tu mquina 68000 ya sigue la Ordenacin de bytes de la red, no necesitas llamar a htonl() sobre tus direcciones IP. Tendras razn, PERO si intentas portar tu cdigo a una mquina que siga la ordenacin contraria tu programa fallar. S portable! Este es un mundo Unix! (Tanto como a Bill Gates le gustara que no lo fuera). Recuerda: dispn tus bytes segn la Ordenacin de bytes de la red antes de ponerlos en la red. Una cuestin final: por qu sin_addr y sin_port necesitan seguir la Ordenacin de bytes de la red, pero sin_family no, estando todos en la misma estructura struct sockaddr_in? La razn es que sin_addr y sin_port se encapsulan en un paquete en los niveles IP y UDP, respectivamente. Por eso, deben seguir la Ordenacin de bytes de la red. Por contra, el ncleo solamente utiliza el campo sin_family para determinar qu tipo de direccin contiene la estructura, as que debe seguir la Ordenacin de bytes de mquina. Adems, como sin_family no se enva a travs de la red, puede preservar la Ordenacin de mquina.
Fjate en que inet_addr() ya devuelve la direccin segn la Ordenacin de bytes de la red-no necesitas llamar a htonl(). Magnfico! Sin embargo, el fragmento de cdigo de arriba no es demasiado robusto porque no se hace ninguna comprobacin de errores. inet_addr() devuelve el valor -1 en caso de error. Recuerdas los nmeros binarios? Resulta que (unsigned) -1 se corresponde con la direccin IP 255.255.255.255! La direccin de difusin. Malo. Recuerda comprobar adecuadamente las condiciones de error. La verdad es que hay una interfaz an ms limpia que puedes usar en lugar de inet_addr(): se llama inet_aton() ("aton" significa " ascii to network"):
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp);
Y a continuacin un ejemplo de cmo se usa al construir una estructura struct sockaddr_in (entenders mejor el ejemplo cuando llegues a las secciones sobre bind() y connect() .)
Pgina 13
struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; // Ordenacin de mquina my_addr.sin_port = htons(MYPORT); // short, Ordenacin de la red inet_aton("10.12.110.57", &(my_addr.sin_addr)); memset(&(my_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura
inet_aton(), en contra de lo que hacen prcticamente todas las otras funciones de sockets, devuelve un valor distinto de cero si tiene xito, y cero cuando falla. Y la direccin se devuelve en inp . Desgraciadamente, no todas las plataformas implementan inet_aton() as que, aunque su uso se recomienda, en esta gua usar inet_addr() que, aunque ms antigua, est ms extendida. Muy bien, ahora ya puedes convertir direcciones IP en formato carcter a su correspondiente representacin binaria. Qu hay del camino inverso? Qu pasa si tienes una estructura struct in_addr y quieres imprimirla en la notacin de cifras y puntos. En ese caso necesitars usar la funcin inet_ntoa() ("ntoa" significa "network to ascii ") segn se muestra a continuacin:
printf("%s", inet_ntoa(ina.sin_addr));
Eso imprimir la direccin IP. Fjate en que inet_ntoa() toma un struct in_addr como argumento, y no un long. Date cuenta tambin de que devuelve un puntero a char. ste apunta a una zona esttica de memoria dentro de inet_ntoa(), as que cada vez que llames a inet_nota() se perder la ltima direccin IP que pediste. Por ejemplo:
char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); a2 = inet_ntoa(ina2.sin_addr); printf("address 1: %s\n",a1); printf("address 2: %s\n",a2);
imprimir:
address 1: 10.12.110.57 address 2: 10.12.110.57
Si necesitas conservar la direccin, usa strcpy() para copiarla a tu propia variable. Eso es todo sobre este asunto por ahora. Ms adelante, aprenders a convertir una cadena como "whitehouse.gov" en su correspondiente direccin IP (Consulta DNS , ms abajo.)
Pgina 14
4. Llamadas al sistema
Esta seccin est dedicada a las llamadas al sistema que te permiten acceder a la funcionalidad de red de una mquina Unix. Cuando llamas a una de estas funciones, el ncleo toma el control y realiza todo el trabajo por ti automgicamente. Lo que ms confunde a la gente es el orden en que deben realizarse las llamadas. En esto las pginas man son completamente intiles, como probablemente ya has descubierto. Como ayuda en una situacin tan desagradable, he tratado de disponer las llamadas al sistema en las siguientes secciones en el orden (ms o menos) exacto en que debes llamarlas en tus programas. Esto, unido con unos pocos fragmentos de cdigo por aqu y por all, un poco de leche con galletas (que me temo que tendrs que aportar t) y un poco de convencimiento y valor, y estars enviando datos a la red como un poseso!
Pero, qu son esos argumentos? En primer lugar, domain tiene que ser "AF_INET", igual que en la estructura struct sochaddr_in de arriba. Adems, el argumento type le dice al ncleo qu tipo de socket es este: SOCK_STREAM o SOCK_DGRAM . Por ltimo, basta con asignar a protocol un "0" para que socket() elija el protocolo correcto en funcin del tipo ( type). (Notas: Hay muchos ms dominios (domain ) de los que yo he listado. Tambin hay muchos ms tipos (type ) de los que yo he listado. Consulta la pgina man de select() . Adems, hay una manera mejor de obtener el protocolo (protocol ). Consulta la pgina man de getprotobyname().) socket() tan slo te devuelve un descriptor de socket que puedes usar en posteriores llamadas al sistema, o -1 en caso de error. En ese caso, a la variable global errno se el asigna un valor de error (consulta la pgina man de perror() .) En alguna documentacin se menciona un valor mstico: "PF_INET ". Se trata de una extraa y etrea bestia que rara vez se deja ver en la naturaleza, pero que voy a clarificar un poco aqu. Hace mucho tiempo se pensaba que tal vez una familia de direcciones (es lo que significa " AF " en "AF_INET": Address Family - familia de direcciones) diversos protocolos que seran referenciados por su familia de protocolos (que es lo que significa "PF" en "PF_INET": Protocol Family - familia de protocolos). Eso nunca ocurri. De acuerdo, lo correcto entonces es usar AF_INET en la estructura struct sockaddr_in y PF_INET en la llamada a socket(). Pero en la prctica puedes usar AF_INET en todas partes. Y puesto que eso es lo que W. Richard Stevens hace en su libro, eso es lo que yo voy a hacer aqu. Bien, bien, bien, pero para qu sirve este socket? La respuesta es que, en s mismo, no sirve
Pgina 15
para gran cosa y necesitas seguir leyendo y hacer ms llamadas al sistema para que esto tenga algn sentido.
sockfd es el descriptor de fichero de socket que devolvi socket(). my_addr es un puntero a una estructura struct sockaddr que contiene informacin acerca de tu propia direccin, a saber, puerto y direccin IP. addrlen se puede asignar a sizeof(struct sockaddr) . Bueno. Esto es demasiado para absorberlo de una sola vez. Veamos un ejemplo:
#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MYPORT 3490 main() { int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); // Comprueba que no hay errores! my_addr.sin_family = AF_INET; // Ordenacin de mquina my_addr.sin_port = htons(MYPORT); // short, Ordenacin de la red my_addr.sin_addr.s_addr = inet_addr("10.12.110.57"); memset(&(my_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura // no olvides comprobar los errores de bind(): bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); . . .
Hay unas pocas cosas a destacar aqu: my_addr.sin_port sigue la Ordenacin de bytes de la red. Tambin la sigue my_addr.sin_addr.s_addr . Otra cosa para estar atentos es que los ficheros de cabecera podran ser distintos en sistemas distintos. Para asegurarte, revisa tus pginas locales de man. Para finalizar con bind(), tendra que mencionar que parte del proceso de obtencin de tu propia direccin IP y/o nmero de puerto puede automatizarse:
my_addr.sin_port = 0; // Elige aleatoriamente un puerto que no est en uso my_addr.sin_addr.s_addr = INADDR_ANY; // usa mi direccin IP
Pgina 16
puerto por ti. Del mismo modo, al asignarle a my_addr.sin_addr.s_addr el valor INADDR_ANY , le ests diciendo que escoja automticamente la direccin IP de la mquina sobre la que est ejecutando el proceso. Si te ests percatando de los detalles tal vez hayas visto que no puse INADDR_ANY en la Ordenacin de bytes de la red. Qu travieso soy. Sin embargo tengo informacin privilegiada: en realidad INADDR_ANY es cero! Cero es siempre cero, aunque reordenes los bytes. Sin embargo, los puristas pueden aducir que podra existir una dimensin paralela donde INADD_ANY fuera, por ejemplo, 12, y que mi cdigo no funcionara all. Est bien:
my_addr.sin_port = htons(0); // Elige aleatoriamente un puerto que no est en uso my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // usa mi direccin IP
Ahora somos tan portables que probablemente no lo creeras. Solamente quera sealarlo, porque en la mayora del cdigo que te encuentres no habrn pasado INADDR_ANY a travs de htonl(). bind() tambin devuelve -1 en caso de error y el asigna a errno el valor del error. Otra cosa a controlar cuando llames a bind(): No uses nmeros de puerto demasiado pequeos. Todos los puertos por debajo del 1024 estn RESERVADOS (a menos que seas el superusuario). Puedes usar cualquier nmero de puerto por encima de ese, hasta el 65535 (siempre y cuando no lo est usando ya otro programa). En ocasiones, puedes encontrarte con que, al volver a ejecutar bind() en un servidor obtienes el error "Address already in use" (La direccin ya se est usando). Qu significa? Bueno, una parte de un socket que estuvo conectado, est todava colgando en el ncleo y est bloqueando el puerto. Puedes esperar a que se libere (alrededor de un minuto) o aadirle cdigo a tu programa permitiendo reutilizar el puerto, de este modo:
int yes=1; //char yes='1'; // La gente de Solaris usa esto // Olvidmonos del error "Address already in use" [La direccin ya se est usando] if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); }
Una nota final ms acerca de bind(): en ocasiones no tendrs que usar esta funcin en absoluto. Si vas a conectar ( connect() ) a una mquina remota y no te importa cul sea tu puerto local (como es el caso de telnet, donde slo te importa el puerto remoto), puedes llamar slo a connect(), que se encargar de comprobar si el puerto est o no asociado y, si es el caso, lo asociar con un puerto local que no se est usando.
Pgina 17
Afortunadamente para ti, programa, ests leyendo ahora la seccin connect() -- o cmo conectar con una mquina remota. As que devralo! No hay tiempo que perder! La llamada connect() es como sigue:
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd es nuestro famoso descriptor de fichero de socket, tal y como nos lo devolvi nuestra llamada a socket(), serv_addr es una estructura struct sockaddr que contiene el puerto y la direccin IP de destino, y a addrlen le podemos asignar el valor sizeof(struct sockaddr). No est esto empezando a tener ms sentido? Veamos un ejemplo:
#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define DEST_IP "10.12.110.57" #define DEST_PORT 23 main() { int sockfd; struct sockaddr_in dest_addr; // Guardar la direccin de destino sockfd = socket(AF_INET, SOCK_STREAM, 0); // Comprueba errores! dest_addr.sin_family = AF_INET; // Ordenacin de mquina dest_addr.sin_port = htons(DEST_PORT); // short, Ordenacin de la red dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); memset(&(dest_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura // no olvides comprobar los errores de connect()! connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)); . . .
Como siempre, asegrate de comprobar el valor de retorno de connect() --devolver -1 en caso de error y establecer la variable errno . Adems fjate en que no hemos llamado a bind(). Bsicamente nos da igual nuestro nmero local de puerto; Slo nos importa a dnde nos dirigimos (el puerto remoto). El ncleo elegir un puerto local por nosotros, y el sitio al que nos conectamos ser informado automticamente. No hay de qu preocuparse.
Pgina 18
sockfd es el habitual descriptor de fichero de socket que nos fue devuelto por la llamada al sistema socket() . backlog es el nmero de conexiones permitidas en la cola de entrada. Qu significa eso? Bueno, las conexiones entrantes van a esperar en esta cola hasta que t las aceptes (accept() ) (mira ms abajo) y ste es el lmite de conexiones que puede haber en cola. La mayora de los sistemas limitan automticamente esta cifra a 20; probablemente puedes apaarte asignando el valor 5 10. Como siempre, listen() devuelve -1 en caso de error y establece errno. Bueno, como probablemente imaginas, necesitamos llamar a bind() antes de poder llamar a listen(), de lo contrario el ncleo nos tendr esperando en un puerto aleatorio. As que si vas a estar escuchando a la espera de conexiones entrantes, la secuencia de llamadas que te corresponde hacer es:
socket(); bind(); listen(); /* accept() va aqu */
Lo doy por vlido como cdigo de ejemplo porque es bastante claro. (El cdigo de la seccin accept(), a continuacin, es ms completo). El autntico truco de todo esto est en la llamada a accept().
sockfd es el descriptor de fichero donde ests escuchando (listen()). Es muy fcil. addr es normalmente un puntero a una estructura struct sockaddr_in local. Ah es donde se guardar la informacin de la conexin entrante (y con ella puedes averiguar que mquina te est llamando, y desde qu puerto). addrlen es un puntero a una variable local int a la que deberas asignar el valor de sizeof(struct sockaddr_in) . accept() pondr dentro de addr un mximo de addrlen bytes. Si pone menos, cambiar el valor de addrlen para que refleje la cantidad real de bytes almacenados. Sabes qu? accept() devuelve -1 en caso de error y establece la variable errno. Debiste suponerlo.
Pgina 19
Como dije antes, esto es un buen cacho para digerirlo de una sola vez, as que ah va un fragmento de cdigo de ejemplo para que te lo estudies:
#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MYPORT 3490 // Puerto al que conectarn los usuarios #define BACKLOG 10 // Cuntas conexiones vamos a mantener en cola main() { int sockfd, new_fd; // se escucha sobre sock_fd, Nuevas conexiones sobre new_fd struct sockaddr_in my_addr; // Informacin sobre mi direccin struct sockaddr_in their_addr; // Informacin sobre la direccin remota int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); // Comprobar errores! my_addr.sin_family = AF_INET; // Ordenacin de mquina my_addr.sin_port = htons(MYPORT); // short, Ordenacin de la red my_addr.sin_addr.s_addr = INADDR_ANY; // Rellenar con mi direccin IP memset(&(my_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura // no olvides comprobar errores para estas llamadas: bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); . . .
Como te deca, usaremos el descriptor de fichero de socket new_fd en las llamadas al sistema send() y recv() . Si solamente esperas recibir una conexin puedes cerrar (close() ) el descriptor sockfd que est escuchando para evitar que lleguen nuevas conexiones de entrada al mismo puerto.
sockfd es el descriptor de socket al que quieres enviar datos (bien sea el devuelto por socket() , bien el devuelto por accept().) msg es un puntero a los datos que quieres enviar, y len es la longitud de esos datos en bytes. Asigna a flags el valor 0 (Revisa la pgina man de send() para ms informacin relativa a los flags). Lo siguiente podra servir como cdigo de ejemplo:
char *msg = "Beej was here!"; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0);
Pgina 20
. . .
send() devuelve el mmero de bytes que se enviaron en realidad--y podran ser menos de los que t pediste que se enviaran! Por ejemplo hay veces que solicitas enviar todo un montn de datos y el sistema sencillamente no puede manejarlos todos. Procesar tantos datos como pueda y confiar en que t enves el resto despus. Recuerda, si el valor devuelto por send() no coincide con el valor de len , depende de ti enviar el resto de la cadena. La buena noticia es esta: si el paquete es pequeo (menos de 1K o as) probablemente se las apaar para enviarlo todo de una tacada. Como siempre, en caso de error devuelve -1 y se establece la variable errno. La llamada al sistema recv() es similar en muchos aspectos:
int recv(int sockfd, void *buf, int len, unsigned int flags);
sockfd es el descriptor del fichero del que se va a leer, buff es el buffer donde se va a depositar la informacin leida, len es la longitud mxima del buffer, y flags, como antes, puede asignarse a 0 (Revisa la pgina man de recv() para informacin sobre flags) recv() devuelve el nmero de bytes que se leyeron en realidad, o -1 en caso de error (y entonces establece errno apropiadamente). Espera! recv() puede devolver 0. Esto slo puede significar una cosa: la mquina remota ha cerrado su conexin contigo! Un valor de retorno igual a cero es la forma que tiene recv() de comunicarte que so ha sucedido. Bueno, fue sencillo, no? Ahora puedes pasar datos en los dos sentidos a travs de un socket de flujo! Estupendo! Eres un programador Unix de redes!
Como ves, esta llamada es bsicamente la misma que send() aadiendo dos items ms de informacin. to es un puntero a una estructura struct sockaddr (probablemente usars una estructura struct sockaddr_in y la forzars a struct sockaddr en el ltimo momento) que contiene la direccin IP y el puerto de destino. Al argumento tolen asgnale el valor sizeof(struct sockaddr). Lo mismo que send(), sendto() devuelve el nmero de bytes que realmente se enviaron
Pgina 21
(que, igual que antes, podran ser menos de los que t pediste enviar) o -1 en caso de error (con errno establecido convenientemente). La misma semejanza presentan recv() y recvfrom(). La sinopsis de recvfrom() es:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
De nuevo, es igual que recv() pero con dos argumentos ms. from es un puntero a una estructura struct sockaddr local que ser rellenada con la direccin IP y el puerto de la mquina de origen. fromlen es un puntero a un int local que tiene que inicializarse a sizeof(struct sockaddr). Cuando la funcin finalice, fromlen contendr la longitud de la direccin almacenada en from. recvfrom() devuelve el nmero de bytes recibidos, o -1 en caso de error (con errno establecido convenientemente). Recuerda, si conectas (connect()) un socket de datagramas simplemente tienes que usar send() y recv() para todas tus transaciones. El socket mismo es an un socket de datagramas y los paquetes seguirn usando UDP, pero el interfaz de sockets aadir automticamente por ti la informacin de origen y destino.
Esto impedir ms lecturas y escrituras al socket. Cualquiera que intente leer o escribir sobre el socket en el extremo remoto recibir un error. Slo en el caso que quieras un poco ms de control sobre cmo se cierra el socket puedes usar la funcin shutdown(). Te permite cortar la comunicacin en un cierto sentido, o en los dos (tal y como lo hace close()). Sinopsis:
int shutdown(int sockfd, int how);
sockfd es el descriptor de socket que quieres desconectar, y how es uno de los siguientes valores: is the socket file descriptor you want to shutdown, and how is one of the following:
shutdown() devuelve 0 si tiene xito, y -1 en caso de error (con errno establecido adecuadamente) Si usas shutdown() en un socket de datagramas sin conexin, simplemente inhabilitar el socket para posteriores llamadas a send() y recv() (recuerda que puedes usarlas si llamaste a connect() sobre tu socket de datagramas). Es importante destacar que shutdown() no cierra realmente el descriptor de fichero --slo cambia sus condiciones de uso. Para liberar un descriptor de socket necesitas usar close(). Nada ms.
sockfd es el descriptor del socket de flujo conectado, addr es un puntero a una estructura struct sockaddr (o struct sockaddr_in) que guardar la informacin acerca del otro lado de la conexin, y addrlen es un puntero a un int, que deberas inicializar a sizeof(struct sockaddr) . La funcin devuelve -1 en caso de error y establece errno apropiadamente. Una vez que tienes su direccin, puedes usar inet_ntoa() or gethostbyaddr() para imprimir u obtener ms informacin. No, no puedes saber su nombre de login. (Vale, vale, si el otro ordenador est ejecutando un demonio ident, s es posible. Esto, sin embargo, va ms all del alcance de este documento. Revisa el RFC-1413 para ms informacin.)
Los argumentos son sencillos: hostname es un puntero a una cadena de carcteres donde se
Pgina 23
almacenar el nombre de la mquina cuando la funcin retorne, y size es la longitud en bytes de esa cadena de caracteres. La funcin devuelve 0 si se completa sin errores, y -1 en caso contrario, estableciendo errno de la forma habitual.
telnet puede averiguar que necesita conectarse (connect()) a "198.137.240.92" Pero, cmo funciona? T vas a usar la funcin gethostbyname() :
#include <netdb.h> struct hostent *gethostbyname(const char *name);
Como puedes ver, devuelve un puntero a una estructura struct hostent , cuyo desglose es el siguiente:
struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0]
h_addr_list -- Un vector terminado en cero de direcciones de red de la mquina. Las direcciones siguen la Ordenacin de bytes de la red.
Pgina 24
gethostbyname() devuelve un puntero a la estructura struct hostent que se ha llenado, o NULL en caso de error. (Sin embargo no se establece errno, sino h_errno . Consulta herror() ms adelante). Pero, cmo se usa? A veces (nos damos cuenta leyendo manuales de informtica), no es suficiente con vomitarle la informacin al lector. En realidad, esta funcin es ms fcil de usar de lo que parece. Aqu hay un programa de ejemplo :
/* ** getip.c -- ejemplo de bsqueda DNS */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { struct hostent *h; if (argc != 2) { // Comprobacin de errores en la lnea de comandos fprintf(stderr,"usage: getip address\n"); exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { // Obtener informacin del host herror("gethostbyname"); exit(1); } printf("Host name : %s\n", h->h_name); printf("IP Address : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr))); return 0; }
Con gethostbyname(), no puedes usar perror() para imprimir mensajes de error (puesto que a errno no se le asigna valor alguno). En su lugar debes llamar a herror() . Est bastante claro. Simplemente pasas la cadena que tiene el nombre de la mquina ("whitehouse.gov") a gethostbyname() , y recuperas la informacin que te han devuelto en la estructura struct hostent. La nica cosa rara que podras encontrarte sera en el momento de imprimir la direccin IP. h>h_addr es un char * mientras que inet_ntoa() necesita una estructura struct in_addr . Por eso, yo suelo forzar h->h_addr a struct in_addr* , y desreferencio para obtener los datos.
Pgina 25
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 mquina remota mediante telnet (el cliente) un programa de aquella mquina (llamado telnetd, el servidor) despierta a la vida. Gestiona la conexin telnet entrante, te presenta una pantalla de login , etc.
El intercambio de informacin 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 mquina determinada, que atender a mltiples clientes usando fork(). El funcionamiento bsico es: el servidor espera una conexin, 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 seccin.
donde remotehostname es el nombre de la mquina donde estas ejecutando. El cdigo servidor : (Nota: una barra invertida al final de una lnea indica que esa lnea se contina 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 conectarn los usuarios
Pgina 26
#define BACKLOG 10 // Cuntas 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; // informacin sobre mi direccin struct sockaddr_in their_addr; // informacin sobre la direccin 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; my_addr.sin_port = htons(MYPORT); red my_addr.sin_addr.s_addr = INADDR_ANY; // Rellenar con mi direccin 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; } // Ordenacin de bytes de la mquina // short, Ordenacin de bytes de la
Por si sientes curiosidad, tengo todo el cdigo en una gran funcin main() porque me parece ms claro. Puedes partirlo en funciones ms pequeas si eso te hace sentir mejor. (Adems, esto del sigaction() podra ser nuevo para ti--es normal. El cdigo que hay ah se
Pgina 27
encarga de limpiar los procesos zombis que pueden aparecer cuando 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 seccin.
Pgina 28
Observa que si no ejecutas el servidor antes de llamar al cliente, connect() devuelve "Connection refused" (Conexin rechazada). Muy til.
Pgina 29
Observa que en nuestra llamada a socket() finalmente estamos usando SOCK_DGRAM. Observa tambin que no hay necesidad de escuchar (listen()) o aceptar (accept()). Esa es una de las ventajas de usar sockets de datagramas sin conexin! A continuacin el cdigo 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; // informacin sobre la direccin 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 informacin de mquina perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // Ordenacin de bytes de mquina their_addr.sin_port = htons(MYPORT); // short, Ordenacin 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 mquina y luego llama a talker en otra. Observa cmo se comunican! Disfruta de toda la excitacin de la familia nuclear entera! Excepto un pequeo detalle que he mencionado muchas veces con anterioridad: sockets de datagramas con conexin. Tengo que hablar de ellos aqu, puesto que estamos en la seccin de datagramas del documento. Supongamos que talker llama a connect() e indica la direccin de listener. A partir de ese momento, talker solamente puede enviar a y recibir de la direccin especificada en connect(). Por ese motivo no tienes que usar sendto() y recvfrom(); tienes que usar simplemente send() y recv().
Pgina 30
6.1. Bloqueo
Bloqueo. Has odo hablar de l--pero qu carajo es? En una palabra, "bloquear" es el tecnicismo para "dormir". Probablemente te has dado cuenta de que, cuando ejecutas listener, ms arriba, simplemente se sienta a esperar a que llegue un paquete. Lo que sucedi es que llam a recvfrom() y no haba datos que recibir. Por eso se dice que recvfrom() se bloquea (es decir, se duerme) hasta que llega algn dato. Muchas funciones se bloquean. accept() se bloquea. Todas las funciones recv() se bloquean. Lo hacen porque les est permitido hacerlo. Cuando creas un descriptor de socket con socket(), el ncleo lo configura como bloqueante. Si quieres que un socket no sea bloqueante, tienes que hacer una llamada a fcntl():
#include <unistd.h> #include <fcntl.h> . . sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . .
Al establecer un socket como no bloqueante, puedes de hecho "interrogar" al socket por su informacin. Si intentas leer de un socket no bloqueante y no hay datos disponibles, la funcin no est autorizada a bloquearse -- devolver -1 y asignar a errno el valor EWOULDBLOCK. En lneas generales, no obstante, este tipo de interrogaciones son una mala idea. Si pones tu programa a esperar sobre un bucle a que un socket tenga datos, estars consumiendo en vano el tiempo de la CPU como se haca antao. Una solucin ms elegante para comprobar si hay datos esperando que se puedan leer, se presenta en la siguiente seccin: select().
Pgina 31
select() te da la posibilidad de comprobar varios sockets al mismo tiempo. Te dir cules estn listos para leer, cules estn listos para escribir, y cules han generado excepciones, si ests interesado en saber eso. Sin ms prembulos, esta es la sinopsis de select() :
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
La funcin comprueba "conjuntos" de descriptores de fichero; en concreto readfds, writefds , y exceptfds. Si quieres saber si es posible leer de la entrada estndar y de un cierto descriptor de socket , sockfd, simplemente aade los descriptores de fichero 0 y sockfd al conjunto readfs. El parmetro numfds debe tener el valor del mayor descriptor de fichero ms uno. En este ejemplo, deberamos asignarle el valor sockfd + 1 , puesto que es seguro que tendr un valor mayor que la entrada estndar (0). Cuando select() regrese, readfs contendr los descriptores de fichero que estn listos para lectura. Puedes comprobarlos con la macro FD_ISSET() que se muestra a continuacin. Antes de progresar ms, te contar como manipular los conjuntos de descriptores. Cada conjunto es del tipo fd_set. Las siguientes macros funcionan sobre ese tipo.
Por ltimo, qu es esa extraa estructura struct timeval? Bueno, a veces no quieres esperar toda la vida a que alguien te enve datos. Quizs cada 96 segundos quieras imprimir "An estoy vivo..." aunque no haya sucedido nada. Esta estructura de tiempo te permite establecer un perodo mximo de espera. Si transcurre ese tiempo y select() no ha encontrado an ningn descriptor de fichero que est listo, la funcin regresar para que puedas seguir procesando. La estructura struct timeval tiene los siguientes campos:
struct timeval { int tv_sec; int tv_usec; }; // segundos // microsegundos
Establece tv_sec al nmero de segundos que quieres esperar, y tv_usec al nmero de microsegundos. S, microsegundos, no milisegundos. Hay 1.000 microsegundos en un milisegundo, y 1.000 milisegundos en un segundo. As que hay 1.000.000 de microsegundos en un segundo. Por qu se llama "usec"? Se supone que la "u" es la letra griega (Mu) que suele usarse para abreviar "micro". Adems, cuando select() regresa, timeout podra
Pgina 32
haberse actualizado al tiempo que queda para que el temporizador indicado expire. Depende del sistema Unix que ests usando. Fantstico! Tenemos un reloj con precisin de microsegundos! No cuentes con ello. El lmite en un sistema Unix estndar est alrededor de los 100 milisegundos, as que seguramente tendrs que esperar eso como mnimo, por muy pequeo que sea el valor con que establezcas la estructura struct timeval. Otras cosas de inters: si estableces los campos de struct timeval a 0, select() regresar inmediatamente despus de interrogar todos tus descriptores de fichero incluidos en los conjuntos. Si estableces el parmetro timeout a NULL el temporizador nunca expirar y tendrs que esperar hasta que algn descriptor de fichero est listo. Por ltimo, si no ests interesado en esperar sobre algn conjunto de descriptores de fichero en particular, slo tienes que usar el parmetro NULL en la llamada a select(). El siguiente fragmento de cdigo espera 2.5 segundos a que algo aparezca por la entrada estndar:
/* ** select.c -- ejemplo de select() */ #include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define STDIN 0 // descriptor de fichero de la entrada estndar int main(void) { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); // no nos preocupemos de writefds and exceptfds: select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf("A key was pressed!\n"); else printf("Timed out.\n"); return 0; }
Si ests en un terminal con buffer de lneas, tendrs que pulsar la tecla RETURN porque en cualquier otro caso el temporizador expirar. Ahora, algunos de vosotros podras pensar que esta es una forma excelente de esperar datos sobre un socket de datagramas --y teneis razn: podra serlo. Algunos Unix permiten usar select() de este modo mientras que otros no. Deberas consultar las pginas locales de man sobre este asunto antes de intentar usarlo. Algunos Unix actualizan el tiempo en struct timeval para mostrar el tiempo que falta para que venza el temporizador. Pero otros no. No confes en que esto ocurra si quieres ser portable. (Usa gettimeofday() si necesitas controlar el tiempo transcurrido. S que es un palo, pero as son las cosas) Qu pasa si un socket en el conjunto de lectura cierra la conexin? En este caso select()
Pgina 33
retorna con el descriptor de socket marcado como "listo para leer". Cuando uses recv(), devolver 0. As es como sabes que el cliente ha cerrado la conexin. Una nota ms de inters acerca de select(): si tienes un socket que est escuchando (listen()), puedes comprobar si hay una nueva conexin poniendo ese descriptor de socket en el conjunto readfs . Y esto, amigos mos, es una visin general sencilla de la todopoderosa funcin select(). Pero, por aclamacin popular, ah va un ejemplo en profundidad. Desgraciadamente, la diferencia entre el sencillo ejemplo anterior y el que sigue es muy importante. Pero chale un vstazo y lee las descripciones que siguen. Este programa acta como un simple servidor multiusuario de chat. Inicialo en una ventana y luego atcalo con telnet ("telnet hostname 9034 ") desde varias ventanas distintas. Cuando escribas algo en una sesin telnet, tiene que aparecer en todas las otras.
/* ** selectserver.c -- servidor de chat multiusuario */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 9034 // puerto en el que escuchamos int main(void) { fd_set master; // conjunto maestro de descriptores de fichero fd_set read_fds; // conjunto temporal de descriptores de fichero para select() struct sockaddr_in myaddr; // direccin del servidor struct sockaddr_in remoteaddr; // direccin del cliente int fdmax; // nmero mximo de descriptores de fichero int listener; // descriptor de socket a la escucha int newfd; // descriptor de socket de nueva conexin aceptada char buf[256]; // buffer para datos del cliente int nbytes; int yes=1; // para setsockopt() SO_REUSEADDR, ms abajo int addrlen; int i, j; FD_ZERO(&master); // borra los conjuntos maestro y temporal FD_ZERO(&read_fds); // obtener socket a la escucha if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } // obviar el mensaje "address already in use" (la direccin ya se est usando) if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt"); exit(1); } // enlazar myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = INADDR_ANY; myaddr.sin_port = htons(PORT); memset(&(myaddr.sin_zero), '\0', 8); if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) { perror("bind");
Pgina 34
exit(1); } // escuchar if (listen(listener, 10) == -1) { perror("listen"); exit(1); } // aadir listener al conjunto maestro FD_SET(listener, &master); // seguir la pista del descriptor de fichero mayor fdmax = listener; // por ahora es ste // bucle principal for(;;) { read_fds = master; // cpialo if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(1); } // explorar conexiones existentes en busca de datos que leer for(i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { // tenemos datos!! if (i == listener) { // gestionar nuevas conexiones addrlen = sizeof(remoteaddr); if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen)) == -1) { perror("accept"); } else { FD_SET(newfd, &master); // aadir al conjunto maestro if (newfd > fdmax) { // actualizar el mximo fdmax = newfd; } printf("selectserver: new connection from %s on " "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd); } } else { // gestionar datos de un cliente if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) { // error o conexin cerrada por el cliente if (nbytes == 0) { // conexin cerrada printf("selectserver: socket %d hung up\n", i); } else { perror("recv"); } close(i); // Hasta luego! FD_CLR(i, &master); // eliminar del conjunto maestro } else { // tenemos datos de algn cliente for(j = 0; j <= fdmax; j++) { // enviar a todo el mundo! if (FD_ISSET(j, &master)) { // excepto al listener y a nosotros mismos if (j != listener && j != i) { if (send(j, buf, nbytes, 0) == -1) { perror("send"); } } } } } } // Esto es TAN FEO! } } } return 0; }
Observa que en el cdigo uso dos conjuntos de descriptores de fichero: master y read_fds.
Pgina 35
El primero, master , contiene todos los descriptores de fichero que estn actualmente conectados, incluyendo el descriptor de socket que est escuchando para nuevas conexiones. La razn por la cual uso el conjunto master es que select() va a cambiar el conjunto que le paso para reflejar qu sockets estn listos para lectura. Como tengo que recordar las conexiones activas entre cada llamada de select(), necesito almacenar ese conjunto en algn lugar seguro. En el ltimo momento copio master sobre read_fs y entonces llamo a select(). Pero, eso no significa que, cada vez que llegue una nueva conexin, tengo que aadirla al conjunto master? Yup! y cada vez que una conexin se cierra, no tengo que borrarla del conjunto master . Efectivamente. Fjate que compruebo si el socket listener est listo para lectura. Si lo est, tengo una nueva conexin pendiente: la acepto (accept() ) y la aado al conjunto master. Del mismo modo, cuando una conexin de cliente est lista para lectura y recv() devuelve 0, s que el cliente ha cerrado la conexin y tengo que borrarlo del conjunto master. Sin embargo, si el cliente recv() devuelve un valor distinto de cero, s que se han recibido datos as que los leo y recorro la lista master para enviarlos a todos los clientes conectados. Y esto, amigos mos, es una visin general no tan sencilla de la todopoderosa funcin select().
En este ejemplo, s es el socket al que quieres enviar los datos, buf es el buffer que contiene
Pgina 36
los datos, y len es un puntero a un int que contiene el nmero de bytes que hay en el buffer. La funcin devuelve -1 en caso de error (y errno est establecido por causa de la llamada a send().) Adems, el nmero de bytes enviados realmente se devuelven en len. Este ser el mismo nmero de bytes que pediste enviar, a menos que sucediera un error. sendall() har lo que pueda tratando de enviar los datos, pero si sucede un error regresar en seguida. Este es un ejemplo completo de cmo usar la funcin:
char buf[10] = "Beej!"; int len; len = strlen(buf); if (sendall(s, buf, &len) == -1) { perror("sendall"); printf("We only sent %d bytes because of the error!\n", len); }
Qu ocurre en el extremo receptor cuando llega un paquete? Si los paquetes tienen longitud variable, cmo sabe el receptor cuando un paquete ha finalizado? Efectivamente, las situaciones del mundo real son un autntico quebradero de cabeza. Probablemente tengas que encapsular tus datos (te acuerdas de esto, en la seccin de encapsulacin de datos ms arriba al principio?) Sigue leyendo para ms detalles!
Y as siempre. Cmo sabe un cliente cundo empieza un mensaje y cundo acaba? Si quisieras, podras hacer que todos los mensajes tuvieran la misma longitud, y simplemente
Pgina 37
llamar a la funcin sendall() que hemos implementado, ms arriba . Pero as desperdicias el ancho de banda! no queremos enviar (send() ) 1024 bytes slo para que "tom" pueda decir "Hi". As que encapsulamos los datos en una pequea estructura de paquete con cabecera. Tanto el servidor como el cliente deben saber cmo empaquetar y desempaquetar los datos (algunas veces a esto se le llama, respectivamente, "marshal" y "unmarshal"). No mires an, pero estamos empezando a definir un protocolo que describe cmo se comunican el servidor y el cliente. En este caso, supongamos que el nombre de usuario tiene una longitud fija de 8 carcteres, rellenados por la derecha con '\0'. y supongamos que los datos son de longitud variable, hasta un mximo de 128 carcteres. Echemos un vistazo a un ejemplo de estructura de paquete que podramos usar en esta situacin: 1. len (1 byte, sin signo) -- La longitud total del paquete, que incluye los 8 bytes del nombre de usuario, y los datos de chat. 2. name (8 bytes) -- El nombre de usuario, completado con carcteres NUL si es necesario. 3. chatdata (n-bytes) -- Los datos propiamente dichos, hasta un mximo de 128 bytes. La longitud del paquete se calcula como la suma de la longitud de estos datos ms 8 (la longitud del nombre de usuario). Porqu eleg limites de 8 y 128 bytes? Los tom al azar, suponiendo que seran lo bastante largos. Sin embargo, tal vez 8 bytes es demasiado restrictivo para tus necesidades, y quieras tener un campo nombre de 30 bytes, o ms. La decisin es tuya. Usando esta definicin de paquete, el primer paquete constara de la siguiente informacin (en hexadecimal y ASCII):
0A (longitud) 74 6F 6D 00 00 00 00 00 T o m (relleno) 48 69 H i
(La longitud sigue la Ordenacin de bytes de la red, por supuesto. En este caso no importa, porque se trata slo de un byte, pero en general, querrs que todos tus enteros binarios de tus paquetes sigan la Ordenacin de bytes de la red). Al enviar estos datos, deberas ser prudente y usar una funcin del tipo de sendall() , as te aseguras de que se envan todos los datos incluso si hacen falta varias llamadas a send() para conseguirlo. Del mismo modo, al recibir estos datos, necesitas hacer un poco de trabajo extra. Para asegurarte, deberas suponer que puedes recibir slo una parte del paquete (por ejemplo, en el caso anterior podramos recibir slo " 00 14 42 65 6E" del nombre "Benjamin" en nuestra llamada a recv() ). As que necesitaremos llamar a recv() una y otra vez hasta que el
Pgina 38
paquete completo se reciba. Pero, cmo? Bueno, sabemos el nmero total de bytes que hemos de recibir para que el paquete est completo, puesto que ese nmero est al principio del paquete. Tambin sabemos que el tamao mximo de un paquete es 1+8+128, es decir 137 bytes (lo sabemos porque as es como hemos definido el paquete) Lo que puedes hacer es declarar un vector [array] lo bastante grande como para contener dos paquetes. Este ser tu buffer de trabajo donde reconstruirs los paquetes a medida que lleguen. Cada vez que recibas (recv()) datos los meters en tu buffer y comprobars si el paquete est compelto. Es decir, si el nmero de bytes en el buffer es mayor o igual a la longitud indicada en la cabecera (+1, porque la longitud de la cabecera no incluye al propio byte que indica la longitud). Si el nmero de bytes en el buffer es menor que 1, el paquete, obviamente, no est completo. Sin embargo, tienes que hacer un caso especial para esto ya que el primer byte es basura y no puedes confiar en que contenga una longitud de paquete correcta. Una vez que el paquete est completo, puedes hacer con l lo que quieras. salo y brralo del buffer. Bueno! An ests dndole vueltas en la cabeza? Bien, ah llega la segunda parte: en una sola llamada a recv(), podras haber ledo ms all del final de un paquete, sobre el principio del siguiente. Es decir, tienes un buffer con un paquete completo y una parte del siguiente paquete. Maldita sea. (Pero esta es la razn por la que hiciste que tu buffer fuera lo bastante grande como para contener dos paquetes-- Por si suceda esto!) Como, gracias a la cabecera, sabes la longitud del primer paquete y adems has controlado cuntos bytes del buffer has usado, con una sencilla resta puedes calcular cuntos de los bytes del buffer corresponden al segundo paquete (incompleto). Cuando hayas procesado el primer paquete puedes borrarlo del buffer y mover el fragmento del segundo paquete al principio del buffer para poder seguir con la siguiente llamada a recv(). (Algunos de vosotros os habeis dado cuenta de que mover el fragmento del segundo paquete al principo de buffer lleva tiempo, y que el programa se puede disear de forma que esto no sea necesario por medio del uso de un buffer circular. Desgraciadamente para el resto, el examen de los buffers circulares va ms all del alcance de este artculo. Si todava sientes curiosidad, pllate un libro de estructuras de datos y consltalo.) Nunca dije que fuera fcil. Est bien, s que lo dije. Y lo es. Slo necesitas prctica y muy pronto resultar para ti de lo ms natural. Lo juro por Excalibur!
Pgina 39
7. Referencias adicionales
Has llegado hasta aqu, y ahora quieres ms! A dnde puedes ir para aprender ms de todo esto?
htonl()
htons()
ntohl()
ntohs()
inet_aton()
inet_addr()
inet_ntoa()
socket()
socket options
bind()
connect()
listen()
accept()
send()
recv()
sendto()
recvfrom()
close()
shutdown()
getpeername()
getsockname()
gethostbyname()
Pgina 40
gethostbyaddr()
getprotobyname()
fcntl()
select()
perror()
gettimeofday()
7.2. Libros
Si buscas libros autnticos que se pueden tocar revisa alguna de las excelentes guas que hay a continuacin. Fjate en el destacado logo de Amazon.com. Lo que esta descarada induccin al consumismo significa es que, bsicamente, obtengo una comisin por vender esos libros desde esta gua (en realidad, obtengo libros). Por eso, si de todas formas vas a comprar uno de esos libros, por qu no enviarme un agradecimiento especial iniciando tu compra desde uno de los enlaces de ah abajo? Adems, ms libros para m redundan, en ltima instancia, en ms guas para ti. ;-)
Unix Network Programming, volumes 1-2 de W. Richard Stevens. Editado por Prentice Hall. ISBNs para los volmenes 1-2: 013490012X , 0130810819 . Internetworking with TCP/IP, volumes I-III de Douglas E. Comer y David L. Stevens. Editado por Prentice Hall. ISBNs para los volmenes I, II, y III: 0130183806 , 0139738436 , 0138487146 . TCP/IP Illustrated, volumes 1-3 de W. Richard Stevens y Gary R. Wright. Editado por Addison Wesley. ISBNs para los volmenes 1, 2, y 3: 0201633469 , 020163354X , 0201634953 . TCP/IP Network Administration de Craig Hunt. Editado por O'Reilly & Associates, Inc. ISBN 1565923227 . Advanced Programming in the UNIX Environment de W. Richard Stevens. Editado por Addison Wesley. ISBN 0201563177 . Using C on the UNIX System de David A. Curry. Editado por O'Reilly & Associates, Inc. ISBN 0937175234. Fuera de catlogo.
programacin en UNIX!) The Unix Socket FAQ Client-Server Computing Intro to TCP/IP (gopher) Internet Protocol Frequently Asked Questions The Winsock FAQ
7.4. RFCs
RFCs --La verdadera materia: RFC-768 --El Protocolo de Datagramas de Usuario (UDP) RFC-791 --El protocolo de Internet (IP) RFC-793 --El Protocolo de Control de Transmisin (TCP) RFC-854 --El Protocolo Telnet RFC-951 --El Protocolo de Arranque [Bootstrap] (BOOTP) RFC-1350 --El Protocolo Trivial de Transferencia de Archivos. (TFTP)
Pgina 42
8. Preguntas ms comunes
Q: De dnde saco esos archivos de cabecera? Q: Qu hago cuando bind() responde "Address already in use" [La direccin ya se est usando]? Q: Cmo puedo obtener una lista de los sockets abiertos en el sistema? Q: Cmo se ve la tabla de encaminamiento? Q: Cmo puedo ejecutar el servidor y el cliente si solamente tengo un ordenador? No necesito una red para escribir programas de redes? Q: Cmo puedo saber si el sistema remoto ha cerrado la conexin? Q: Cmo se implementa la utilidad "ping"? Qu es ICMP? Dnde puedo averiguar ms cosas sobre los sockets puros [raw sockets]? Q: Cmo compilo sobre Windows ? Q: Cmo compilo sobre Solaris/SunOS? Todava tengo errores al enlazar! Q: Porque termina select() al recibir una seal? Q: Cmo puedo implementar un temporizador [timeout] en una llamada a recv() ? Q: Cmo puedo comprimir o encriptar los datos antes the enviarlos al socket? Q: Qu es eso de " PF_INET" tiene algo que ver con AF_INET ? Q: Cmo puedo escribir un servidor que acepte comandos de shell de un cliente y los ejecute? Q: Estoy enviando un montn de datos, pero cuando hago recv(), slo recibo 536 bytes 1460 bytes a la vez. Sin embargo, si ejecuto en mi mquina local recibo todos los datos al mismo tiempo. Qu pasa? Q: Yo uso Windows y no tengo la llamada al sistema fork() ni ningn tipo de estructura struct sigaction. Qu hago? Q: Cmo puedo enviar datos seguros con TCP/IP usando encriptacin? Q: Estoy detrs de un cortafuegos [firewall]--Cmo informo a la gente al otro del cortafuegos de cual es mi direccin IP para que puedan conectarse a mi mquina?
Pgina 43
Q: De dnde saco esos archivos de cabecera? A: Si no estn ya en tu sistema, probablemente no los necesitas. Consulta el manual de tu plataforma. Si ests compilando en Windows slo necesitas incluir #include <winsock.h>. Q: Qu hago cuando bind() responde "Address already in use" [La direccin ya se est usando]? A: Tienes que usar setsockopt() con la opcin SO_REUSEADDR sobre el socket en el que ests escuchando ( listen() ). Consulta la seccin sobre bind() y la seccin sobre select() para ver un ejemplo. Q: Cmo puedo obtener una lista de los sockets abiertos en el sistema? A: Usa netstat . Revisa la pgina man para conocer a fondo los detalles, pero deberas tener un resultado aceptable con slo teclear:
$ netstat
El nico truco consiste en averiguar qu socket est asociado con qu programa. :-) Q: Cmo se ve la tabla de encaminamiento? A: Usa el comando route (En la mayora de Linuxes lo encontrars en /sbin) o el comando netstat -r. Q: Cmo puedo ejecutar el servidor y el cliente si solamente tengo un ordenador? No necesito una red para escribir programas de redes? A: Afortunadamente para ti, no. Virtualmente todas las mquinas implementan un "dispositivo" de red de cierre de circuito [loopback] que reside en el ncleo y simula ser una tarjeta de red. (Este es el interfaz que se lista como "lo" en la tabla de encaminamiento.) Supn que has iniciado sesin en una mquina que se llama "goat". Ejecuta el cliente en una ventana y el servidor en otra. O inicia el servidor en segundo plano [background] (escribe "servidor &") y ejecuta el cliente en la misma ventana. Como consecuencia del dispositivo de cierre de circuito puedes sencillamente ejecutar cliente goat o cliente localhost (puesto que "localhost", muy probablemente, est definido en tu fichero /etc/hosts ) y tu cliente estar hablando de inmediato con el servidor. En resumen, no es necesario cambiar nada para que el cdigo funcione en una sola mquina sin conexin a la red! Magnfico! Q: Cmo puedo saber si el sistema remoto ha cerrado la conexin? A: Lo sabes porque recv() devuelve 0. Q: Cmo se implementa la utilidad "ping"? Qu es ICMP? Dnde puedo averiguar ms cosas sobre los sockets puros [raw sockets]?
Pgina 44
A: Todas tus dudas referidas a sockets puros tienen respuesta en los libros de programacin UNIX en redes de W. Richard Stevens. Consulta la seccin de libros de esta gua. Q: Cmo compilo sobre Windows ? A: Empieza borrando Windows e instalando en su lugar Linux o BSD. };-) . No, en realidad slo has de seguir las recomendaciones de la seccin sobre compilacin en Windows de la introduccin. Q: Cmo compilo sobre Solaris/SunOS ? Todava tengo errores al enlazar! A: Los errores de enlace suceden porque las mquinas Sun no enlazan automticamente las bibliotecas de sockets. Consulta la seccin sobre compilacin en Solaris/SunOS de la introduccin, donde hallars un ejemplo de cmo hacerlo. Q: Por qu termina select() al recibir una seal? A: Las seales suelen causar que las llamadas al sistema que estn bloqueadas devuelvan el valor -1 con errno establecifo a EINTR. Cuando preparas un manejador de seales con sigaction() , puedes establecer el indicador SA_RESTART, que se supone que reiniciar la llamada al sistema que fue interrumpida. Naturalmente, esto no siempre funciona. Mi solucin favorita a esto, requiere el uso de una sentencia goto. Sabes que esto irrita a tus profesores sobremanera, as que al ataque!
select_restart: if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) { if (errno == EINTR) { // Alguna seal nos ha interrumpido, as que regresemos a select() goto select_restart; } // Los errores reales de select() se manejan aqu: perror("select"); }
Por supuesto que, en este caso, no necesitas una sentencia goto; puedes usar otras estructuras de control. Pero creo que goto es una solucin ms limpia. Q: Cmo puedo implementar un temporizador [timeout] en una llamada a recv()? A: Usa select() ! Te permite indicar un parmetro de control de tiempo sobre los descriptores de socket en los que esperas leer. Tambin puedes implementar toda esa funcionalidad en una nica funcin como esta:
#include <unistd.h> #include <sys/time.h> #include <sys/types.h> #include <sys/socket.h> int recvtimeout(int s, char *buf, int len, int timeout) { fd_set fds; int n; struct timeval tv; // Construir el conjunto de descriptores de fichero
Pgina 45
FD_ZERO(&fds); FD_SET(s, &fds); // Construir la estructura timeval del temporizador tv.tv_sec = timeout; tv.tv_usec = 0; // Esperar hasta que se reciban datos o venza el temporizador n = select(s+1, &fds, NULL, NULL, &tv); if (n == 0) return -2; // El temporizador ha vencido! if (n == -1) return -1; // error // Los datos deben estar ah, as que llamo normalmente a recv() return recv(s, buf, len, 0); } // Ejemplo de llamada a recvtimeout(): . . n = recvtimeout(s, buf, sizeof(buf), 10); // 10 segundos de espera if (n == -1) { // ocurri un error perror("recvtimeout"); } else if (n == -2) { // El temporizador venci } else { // Hay datos en el buffer } . .
Nota que recvtimeout() devuelve -2 en caso de que venza el temporizador. Por qu no devolver 0? Bueno, si recuerdas un valor de retorno de 0 para una llamada a recv() significa que la mquina remota ha cerrado la conexin. As que ese valor de retorno ya tiene un significado. El valor -1 significa "error", as que escog -2 como mi indicador de temporizador vencido. Q: Cmo puedo comprimir o encriptar los datos antes de enviarlos al socket? A: Una forma sencilla de encriptar es usar SSL (secure sockets layer), pero eso va ms all del alcance de esta gua. Pero suponiendo que quieres implementar tu propio sistema de compresin o encriptado, slo es cuestin de pensar que tus datos siguen una secuencia de etapas entre ambos extremos. Cada paso altera los datos en cierta manera. 1. El servidor lee datos de un fichero (o de donde sea) 2. El servidor encripta los datos (t aades esta parte) 3. El servidor enva ( send() ) los datos encriptados Y ahora el camino inverso: 4. El cliente recibe ( recv() ) los datos encriptados 5. El cliente desencripta los datos (t aades esta parte) 6. El cliente escribe los datos en un archivo (o donde sea) Podras comprimir los datos en el punto en que arriba se encritan/desencriptan los datos. O
Pgina 46
podras hacer las dos cosas!. Sencillamente recuerda comprimir antes de encriptar. :) En la medida en que el cliente deshaga correctamente lo que el servidor haga, los datos llegarn correctamente al final, sin importar cuntos pasos intermedios se aadan. As que todo lo que necesitas para usar mi cdigo es encontrar el lugar situado entre la lectura y el envo ( send() ) de los datos, y poner all el cdigo que se encarga de realizar el encriptado. Q: Qu es eso de " PF_INET" ? Tiene algo que ver con AF_INET ? A: Por supuesto que s. Revisa la seccin socket() para los detalles. Q: Cmo puedo escribir un servidor que acepte comandos de shell de un cliente y los ejecute? A: Para simplificar las cosas, digamos que el cliente conecta ( connect() ), enva ( send() ), y cierra ( close() ) la conexin (es decir no hay llamadas al sistema posteriores sin que el cliente vuelva a conectar.) El proceso que sigue el cliente es este: 1. connect() con el servidor 2. send("/sbin/ls > /tmp/client.out") 3. close() la conexin Mientras tanto el servidor procesa los datos de esta manera: 1. accept() la conexin del cliente 2. recv(str) el comando a ejecutar 3. close() la conexin 4. system(str) ejecuta el comando Cuidado! Dejar que el servidor ejecute lo que el cliente dice es como dar acceso remoto al shell y la gente podra hacer cosas no deseadas con tu cuenta cuando se conectan al servidor. Por ejemplo, en el ejemplo anterior, qu pasa si el cliente enva "rm -rf * "? Borra todo lo que tengas en tu cuenta, eso es lo que pasa! As que aprendes la leccin e impides que el cliente use cualquier comando, a excepcin de un par de utilidades que sabes que son seguras, como la utilidad foobar :
if (!strcmp(str, "foobar")) { sprintf(sysstr, "%s > /tmp/server.out", str); system(sysstr); }
~ "? Lo mejor que se puede hacer es escribir una pequea rutina que ponga un carcter de escape("\") delante de cualquier carcter no alfanumrico (incluyendo espacios, si es necesario) en los argumentos para el comando. Como puedes ver, la seguridad es un problema bastante importante cuando el servidor comienza a ejecutar cosas que el cliente enva. Q: Estoy enviando un montn de datos, pero cuando hago recv(), slo recibo 536 bytes 1460 bytes a la vez. Sin embargo, si ejecuto en mi mquina local recibo todos los datos al mismo tiempo. Qu pasa? A: Te ests tropezando con la MTU (Maximum Transfer Unit)--El tamao mximo de paquete que el medio fsico puede manejar. En la mquina local ests usando el dispositivo de cierre de circuito [loopback] que puede manejar sin problemas 8K o ms. Pero en una Ethernet, que slo puede manejar 1500 bytes con una cabecera, tienes que plegarte a ese lmite. Sobre un mdem, con una MTU de 576 (con cabecera), has de plegarte a un lmite an menor. En primer lugar tienes que asegurarte de que todos los datos se estn enviando. (Revisa la implementacin de la funcin sendall() para ver los detalles). Una vez que ests seguro de eso, tienes que llamar a recv() en un bucle hasta que todos los datos se hayan ledo. Revisa la seccin Consecuencias de la encapsulacin de datos para los detalles acerca de como recibir paquetes completos de datos usando mltiples llamadas a recv(). Q: Yo uso Windows y no tengo la llamada al sistema fork() ni nign tipo de estructura struct sigaction . Qu hago? A: De estar en algn sitio estarn en las bibliotecas POSIX que quizs hayan venido con tu compilador. Como no tengo Windows, en realidad no puedo darte la respuesta, pero creo recordar que Microsoft tiene una capa de compatibilidad POSIX , y ah es donde fork() tendra que estar (y a lo mejor tambin sigaction.) Busca "fork" o "POSIX" en la ayuda de VC++, por si te da alguna pista. Si no hay forma de que funcione, olvdate de fork()/sigaction y usa en su lugar la funcin equivalente de Win32: CreateProcess(). No s cmo se usa CreateProcess() --Tiene tropecientos argumentos, pero seguramente est explicada en la ayuda de VC++ Q: Cmo puedo enviar datos seguros con TCP/IP usando encriptacin? A: Visita el Proyecto OpenSSL. Q: Estoy detrs de un cortafuegos [firewall]--Cmo informo a la gente del otro lado del cortafuegos de cual es mi direccin IP para que puedan conectarse a mi mquina? A: Desgraciadamente, la finalidad de un cortafuegos es evitar que la gente del otro lado del cortafuegos pueda conectarse a las mquinas de este lado del cortafuegos, as que permitir que lo hagan se considera en principio una brecha de seguridad. No es que diga que todo est perdido. Por una parte, todava puedes conectar ( connect() ) a
Pgina 48
travs del cortafuegos si ste est realizando algn tipo de enmascaramiento [masquerading] o NAT [Network Address Translation - Traduccin de direcciones de red] o algo por el estilo. Tan slo tienes que disear tus programas de modo que seas t siempre quien inicia la conexin, y todo ir bien. Si esta solucin no es satisfactoria, puedes pedir al administrador de tu sistema que practique un agujero en el cortafuegos de modo que la gente pueda conectarse contigo. El cortafuegos puede reenviarte sus datos, bien mediante el software NAT, bien mediante un proxy o algo similar. Ten en cuenta que un agujero en el cortafuegos no es algo que pueda uno tomarse a la ligera. Tienes que estar muy seguro de que no das acceso a la red interior a personas con malas intenciones; si eres un principiante, hacer software seguro es mucho ms difcil de lo que puedas imaginar. No hagas que el administrador de tu sistema se enfade conmigo. ;-)
Pgina 49
Pgina 50