Docsity
Docsity

Prepara tus exámenes
Prepara tus exámenes

Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity


Consigue puntos base para descargar
Consigue puntos base para descargar

Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium


Orientación Universidad
Orientación Universidad

2. ¿Qué es un socket Continuamente oyes hablar de los " sockets ", y tal vez t, Apuntes de Ingeniería Infórmatica

Asignatura: Lab. de Redes, Profesor: , Carrera: Ingeniería Informática, Universidad: UAX

Tipo: Apuntes

Antes del 2010

Subido el 22/05/2007

_vivayo_
_vivayo_ 🇪🇸

3.7

(116)

149 documentos

1 / 23

Toggle sidebar

Documentos relacionados


Vista previa parcial del texto

¡Descarga 2. ¿Qué es un socket Continuamente oyes hablar de los " sockets ", y tal vez t y más Apuntes en PDF de Ingeniería Infórmatica solo en Docsity! 2. ¿Qué es un socket? Continuamente oyes hablar de los "sockets", y tal vez te estás preguntando qué es lo que son exactamente. Bien, esto es lo que son: una forma de comunicarse con otros programas usando descriptores de fichero estándar de Unix. ¿Qué? Vale--puede que hayas oído a algún 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 más que un entero asociado a un archivo abierto. Pero (y aquí está el quid de la cuestión) ese fichero puede ser una conexión 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 través de Internet vas a tener que hacerlo con un descriptor de fichero, es mejor que lo creas. "¿De dónde saco yo este descriptor de fichero para comunicar a través de la red, señor 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 través de un socket ." La respuesta corta es, "¡Sí que puedes!" La respuesta larga es, "Puedes usarlas, pero send() y write() te ofrecen mucho más control sobre la transmisión 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 más en función de la modalidad de Unix que estés ejecutando. Este docuemento se ocupa únicamente de los primeros: los sockets de Internet. 2.1. Dos tipos de sockets de internet ¿Qué es esto? ¿Hay dos tipos de sockets de internet? Sí. Bueno no, estoy mintiendo. En realidad hay más pero no quería asustarte. Aquí sólo voy a hablar de dos tipos. A excepción de esta frase, en la que voy a mencionar que los sockets puros [Raw Sockets] son también muy potentes y quizás quieras buscarlos más adelante. Muy bien. ¿Cuáles son estos dos tipos? El primer tipo de sockets lo definen los sockets de flujo [Stream sockets]; El otro, los sockets de datagramas [Datagram sockets], a los que en adelante me referiré, respectivamente como "SOCK_STREAM" y " SOCK_DGRAM ". En ocasiones, a los sockets de datagramas se les llama también "sockets sin conexión". (Aunque se puede usar connect() con ellos, si se quiere. Consulta connect() , más abajo.) Los sockets de flujo definen flujos de comunicación en dos direcciones, fiables y con conexión. Si envías dos ítems a través del socket en el orden "1, 2" llegarán al otro extremo en el orden "1, 2", y llegarán sin errores. Cualquier error que encuentres es producto de tu extraviada mente, y no lo vamos a discutir aquí. ¿Qué aplicación tienen los sockets de flujo? Bueno, quizás has oído hablar del programa telnet. ¿Sí? telnet usa sockets de flujo. Todos los carácteres que tecleas tienen que llegar en el mismo orden en que tú los tecleas, ¿no? También los navegadores, que usan el protocolo HTTP , usan sockets de flujo para obtener las páginas. De hecho, si haces telnet a un sitio de la web sobre el puerto 80, y escribes " GET / ", recibirás como respuesta el código HTML. ¿Cómo consiguen los sockets de flujo este alto nivel de calidad en la transmisión de datos? Usan un protocolo llamado "Protocolo de Control de Transmisión", más conocido como "TCP" (consulta el RFC-793 para información extremadamente detallada acerca de TCP). TCP asegura que tu información llega secuencialmente y sin errores. Puede que hayas oído antes "TCP" como parte del acrónimo " TCP/IP ", donde "IP" significa "Protocolo de Internet" (consulta el RFC-791 .) IP se encarga básicamente del encaminamiento a través 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 conexión? ¿De qué va esto, en definitiva, y por qué no son fiables? Bueno, estos son los hechos: si envías un datagrama, puede que llegue. Puede que llegue fuera de secuencia. Si llega, los datos que contiene el paquete no tendrán errores. Los sockets de datagramas también 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 conexión? Bueno, básicamente porque no tienes que mantener una conexión abierta como harías con los sockets de flujo. Simplemente montas un paquete, le metes una cabecera IP con la información de destino y lo envías. No se necesita conexión. Generalmente se usan para transferencias de información por paquetes. Aplicaciones que usan este tipo de sockets son, por ejemplo, tftp y bootp. "¡Basta!" puede que grites. "¿Cómo pueden siquiera funcionar estos programas si los datagramas podrían 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. 2.2. Tonterías de bajo nivel y teoría de redes Puesto que acabo de mencionar la disposición en capas de los protocolos, es el momento de hablar acerca de como las redes funcionan realmente, y de mostrar algunos ejemplos de como se construyen los paquetes SOCK_DGRAM . Probablemente, en la práctica puedes saltarte esta sección. Sin embargo no está mal como culturilla. Figura 1. Encapsulación de datos. ¡Eh tíos, es hora de aprender algo sobre Encapsulación de datos ! Esto es muy, muy importante. Tan importante que podrías aprenderlo si te matricularas en el curso de redes aquí, en el estado de Chico ;-). Básicamente 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 (físico) del hardware (por ejemplo, Ethernet) Cuando otro ordenador recibe el paquete, el hardware retira la cabecera Ethernet, el núcleo [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 cómo los datos se transmiten físicamente (serie, thin ethernet, AUI, lo que sea) porque los programas en los niveles más bajos se ocupan de eso por ti. El hardware y la topología real de la red son transparentes para el programador de sockets. Sin más preámbulos, te presento los niveles del modelo completo. Recuerda esto para tus exámenes de redes: • Aplicación • Presentación Muy bien. Existen dos tipos sobre los cuales podemos aplicar la conversión: short (dos bytes) y long (cuatro bytes ). Las funciones de conversión también funcionan con las respectivas versiones unsigned de short y long. Imagina que quieres convertir un short desde la Ordenación de máquina [Host Byte Order] a la Ordenación de la red [Network byte order]. Empieza con una "h" de "host", síguela con "to" (a, hacia,...), luego una "n" de "network " y finalmente una "s" de "short": h-to-n-s, es decir, htons() (se lee: "Host to Network Short" -" short de máquina a short de la red") Casi resulta demasiado sencillo... Puedes usar cualquier combinación de "n", "h", "s" y "l" (de long ), sin contar las absurdas. Por ejemplo, NO hay ninguna función stolh() ("Short to Long Host" -- short de máquina a long de máquina). Por lo menos no la hay en lo que a nosotros nos concierne. Sin embargo sí que existen: • htons() -- "Host to Network Short " (short de máquina a short de la red) • htonl() -- "Host to Network Long" (long de la máquina a long de la red) • ntohs() -- "Network to Host Short " (short de la red a short de máquina) • ntohl() -- "Network to Host Long" (long de la red a long de máquina) Ahora, puedes creer que le estás cogiendo el truco a esto. Podrías pensar, "¿Qué pasa si tengo que cambiar la Ordenación de bytes de un char ?", entonces podrías pensar, "Bueno, en realidad no importa". También podrías pensar que, puesto que tu máquina 68000 ya sigue la Ordenación de bytes de la red, no necesitas llamar a htonl() sobre tus direcciones IP. Tendrías razón, PERO si intentas portar tu código a una máquina que siga la ordenación contraria tu programa fallará. ¡Sé portable! ¡Este es un mundo Unix! (Tanto como a Bill Gates le gustaría que no lo fuera). Recuerda: dispón tus bytes según la Ordenación de bytes de la red antes de ponerlos en la red. Una cuestión final: ¿por qué sin_addr y sin_port necesitan seguir la Ordenación de bytes de la red, pero sin_family no, estando todos en la misma estructura struct sockaddr_in? La razón es que sin_addr y sin_port se encapsulan en un paquete en los niveles IP y UDP, respectivamente. Por eso, deben seguir la Ordenación de bytes de la red. Por contra, el núcleo solamente utiliza el campo sin_family para determinar qué tipo de dirección contiene la estructura, así que debe seguir la Ordenación de bytes de máquina. Además, como sin_family no se envía a través de la red, puede preservar la Ordenación de máquina. 3.2. Direcciones IP y como tratarlas Afortunadamente para ti, hay un montón de funciones que te permiten manejar direcciones IP. No hay necesidad de complicarse usando el operador << sobre un long, ni cosas así. Para empezar, supón que tienes una estructura struct sockaddr_in ina , y que quieres guardar en ella la dirección IP " 10.12.110.57 ". La función que necesitas usar, inet_addr(), convierte una dirección IP dada en la notación de cifras y puntos en un unsigned long . La asignación se puede hacer así: ina.sin_addr.s_addr = inet_addr("10.12.110.57"); Fíjate en que inet_addr() ya devuelve la dirección según la Ordenación de bytes de la red--no necesitas llamar a htonl(). ¡Magnífico! Sin embargo, el fragmento de código de arriba no es demasiado robusto porque no se hace ninguna comprobación de errores. inet_addr() devuelve el valor -1 en caso de error. ¿Recuerdas los números binarios? ¡Resulta que (unsigned) -1 se corresponde con la dirección IP 255.255.255.255! La dirección de difusión. Malo. Recuerda comprobar adecuadamente las condiciones de error. La verdad es que hay una interfaz aún más 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 continuación un ejemplo de cómo se usa al construir una estructura struct sockaddr_in (entenderás mejor el ejemplo cuando llegues a las secciones sobre bind () y connect() .) struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; // Ordenación de máquina my_addr.sin_port = htons(MYPORT); // short, Ordenación 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 prácticamente todas las otras funciones de sockets, devuelve un valor distinto de cero si tiene éxito, y cero cuando falla. (Si alguien sabe porqué, por favor que me lo diga.) La dirección se almacena en inp . Desgraciadamente, no todas las plataformas implementan inet_aton() así que, aunque su uso se recomienda, en esta guía usaré inet_addr() que, aunque más antigua, está más extendida. Muy bien, ahora ya puedes convertir direcciones IP en formato carácter a su correspondiente representación binaria. ¿Qué hay del camino inverso? Qué pasa si tienes una estructura struct in_addr y quieres imprimirla en la notación de cifras y puntos. En ese caso necesitarás usar la función inet_ntoa() ("ntoa" significa "network to ascii ") según se muestra a continuación: printf("%s", inet_ntoa(ina.sin_addr)); Eso imprimirá la dirección IP. Fíjate en que inet_ntoa() toma un struct in_addr como argumento, y no un long. Date cuenta también de que devuelve un puntero a char. Éste apunta a una zona estática de memoria dentro de inet_ntoa(), así que cada vez que llames a inet_nota() se perderá la última dirección IP que pediste. Por ejemplo: char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); // esta es 192.168.4.14 a2 = inet_ntoa(ina2.sin_addr); // esta es 10.12.110.57 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 dirección, usa strcpy() para copiarla a tu propia variable. Esto es todo sobre este asunto por ahora. Más adelante, aprenderás a convertir una cadena como "whitehouse.gov" en su correspondiente dirección IP (Consulta DNS , más abajo.) 4. Llamadas al sistema Esta sección está dedicada a las llamadas al sistema que te permiten acceder a la funcionalidad de red de una máquina Unix. Cuando llamas a una de estas funciones, el núcleo toma el control y realiza todo el trabajo por ti automágicamente. Lo que más confunde a la gente es el orden en que deben realizarse las llamadas. En esto las páginas man son completamente inútiles, como probablemente ya has descubierto. Como ayuda en una situación tan desagradable, he tratado de disponer las llamadas al sistema en las siguientes secciones en el orden (más o menos) exacto en que debes llamarlas en tus programas. Esto, unido con unos pocos fragmentos de código por aquí y por allí, un poco de leche con galletas (que me temo que tendrás que aportar tú) y un poco de convencimiento y valor, ¡y estarás enviando datos a la red como un poseso! 4.1. socket() --¡Consigue el descriptor de fichero! Supongo que ya no puedo postponerlo más--Tengo que hablarte de la llamada al sistema socket(). Ahí van los detalles: #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); Pero, ¿qué son esos argumentos? En primer lugar, domain tiene que ser "AF_INET", igual que en la estructura struct sochaddr_in de arriba. Además, el argumento type le dice al núcleo 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 función del tipo ( type). (Notas: Hay muchos más dominios (domain ) de los que yo he listado. También hay muchos más tipos (type ) de los que yo he listado. Consulta la página man de select() . Además, hay una manera mejor de obtener el protocolo (protocol ). Consulta la página man de getprotobyname().) socket() tan sólo 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 página man de perror() .) En alguna documentación se menciona un valor místico: "PF_INET ". Se trata de una extraña y etérea 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 serían 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 práctica 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 para gran cosa y necesitas seguir leyendo y hacer más llamadas al sistema para que esto tenga algún sentido. 4.2. bind()--¿En qué puerto estoy? Una vez que tienes tu socket, tendrías que asociarlo con algún puerto de tú máquina local. (Esto es lo que comúnmente se hace si vas a escuchar [listen()] a la espera de conexiones entrantes sobre un puerto específico--Esto es lo que hacen cuando te dicen que hagas a telnet a x.y.z puerto 6969). El núcleo usa el número de puerto para asociar los paquetes entrantes con un descriptor de socket de un cierto proceso. Si solamente vas a hacer un connect() esto es innecesario. De todas formas léelo, sólo por saberlo. Esta es la sinopsis de la llamada al sistema bind() : #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 dirección de destino sockfd = socket(AF_INET, SOCK_STREAM, 0); // ¡Comprueba errores! dest_addr.sin_family = AF_INET; // Ordenación de máquina dest_addr.sin_port = htons(DEST_PORT); // short, Ordenación 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, asegúrate de comprobar el valor de retorno de connect() --devolverá -1 en caso de error y establecerá la variable errno . Además fíjate en que no hemos llamado a bind(). Básicamente nos da igual nuestro número local de puerto; Sólo nos importa a dónde nos dirigimos (el puerto remoto). El núcleo elegirá un puerto local por nosotros, y el sitio al que nos conectamos será informado automáticamente. No hay de qué preocuparse. 4.4. listen() --Por favor, que alguien me llame Muy bien, es momento para un cambio de ritmo. ¿Qué pasa si no te quieres conectar a una máquina remota? Supongamos, sólo por probar, que quieres esperar a que te lleguen conexiones de entrada y gestionarlas de alguna manera. El proceso consta de dos pasos: en primer lugar escuchas (listen() ) y después aceptas ( accept() ) (mira más abajo) La llamada listen() es bastante sencilla, pero requiere una breve explicación: int listen(int sockfd, int backlog); sockfd es el habitual descriptor de fichero de socket que nos fue devuelto por la llamada al sistema socket() . backlog es el número 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 más abajo) y éste es el límite de conexiones que puede haber en cola. La mayoría de los sistemas limitan automáticamente esta cifra a 20; probablemente puedes apañarte 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 núcleo 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 válido como código de ejemplo porque es bastante claro. (El código de la sección accept(), a continuación, es más completo). El auténtico truco de todo esto está en la llamada a accept(). 4.5. accept() --"Gracias por llamar al puerto 3490." Prepárate--la llamada al sistema accept() es un tanto extraña. Lo que va a suceder es lo siguiente: alguien muy, muy lejano intentará conectar (connect() ) con tu máquina en un puerto en el que tú estás escuchando (listen()). Su conexión pasará a cola, esperando a ser aceptada ( accept() ). Cuando llamas a accept() le estás diciendo que quieres obtener una conexión pendiente. La llamada al sistema, a su vez, te devolverá un descriptor de fichero de socket completamente nuevo para que lo uses en esta nueva conexión. Exacto. De repente, y por el precio de uno, tienes dos descriptores de fichero de socket. El original está todavía escuchando en tu puerto, y el de nueva creación está lista para enviar (send()) y recibir (recv()). ¡Ya casi estamos ahí! La llamada es como sigue: #include <sys/socket.h> int accept(int sockfd, void *addr, int *addrlen); sockfd es el descriptor de fichero donde estás escuchando (listen()). Es muy fácil. addr es normalmente un puntero a una estructura struct sockaddr_in local. Ahí es donde se guardará la información de la conexión entrante (y con ella puedes averiguar que máquina te está llamando, y desde qué puerto). addrlen es un puntero a una variable local int a la que deberías asignar el valor de sizeof(struct sockaddr_in) . accept() pondrá dentro de addr un máximo 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. Como dije antes, esto es un buen cacho para digerirlo de una sola vez, así que ahí va un fragmento de código 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 conectarán los usuarios #define BACKLOG 10 // Cuántas 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; // Información sobre mi dirección struct sockaddr_in their_addr; // Información sobre la dirección remota int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); // ¡Comprobar errores! my_addr.sin_family = AF_INET; // Ordenación de máquina my_addr.sin_port = htons(MYPORT); // short, Ordenación de la red my_addr.sin_addr.s_addr = INADDR_ANY; // Rellenar con mi dirección IP memset(&(my_addr.sin_zero), '\0', 8); // Poner a cero el resto de la estructura // 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 decía, usaremos el descriptor de fichero de socket new_fd en las llamadas al sistema send() y recv() . Si solamente esperas recibir una conexión puedes cerrar (close() ) el descriptor sockfd que está escuchando para evitar que lleguen nuevas conexiones de entrada al mismo puerto. 4.6. send() y recv()--¡Háblame, baby! Estas dos funciones sirven para comunicarse a través de sockets de flujo o sockets de datagramas conectados. Si quieres usar sockets de datagramas desconectados normales tienes que leer la sección sendto() y recvfrom() , a continuación. La llamada al sistema send(): int send(int sockfd, const void *msg, int len, int flags); 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 página man de send() para más información relativa a los flags). is the socket descriptor you want to send data to (whether it's the one returned by socket() or the one you got with accept() .) msg is a pointer to the data you want to send, and len is the length of that data in bytes. Just set flags to 0. (See the send() man page for more information concerning flags.) Lo siguiente podría servir como código de ejemplo: char *msg = "Beej was here!"; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); . . . send() devuelve el múmero de bytes que se enviaron en realidad--¡y podrían ser menos de los que tú pediste que se enviaran! Por ejemplo hay veces que solicitas enviar todo un montón de datos y el sistema sencillamente no puede manejarlos todos. Procesará tantos datos como puede y confía en que tu envíes el resto después. 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 pequeño (menos de 1K o así) probablemente se las apañará 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 información leida, len es la longitud máxima del buffer, y flags, como antes, puede asignarse a 0 (Revisa la página man de recv() para información sobre flags) ¿Qué puede haber más divertido? Se me ocurren un par de cosas, pero no tienen nada que ver con la programación de sockets. En todo caso, ahí van los detalles: #include <unistd.h> int gethostname(char *hostname, size_t size); Los argumentos son sencillos: hostname es un puntero a una cadena de carácteres donde se almacenará el nombre de la máquina cuando la función retorne, y size es la longitud en bytes de esa cadena de caracteres. La función devuelve 0 si se completa sin errores, y -1 en caso contrario, estableciendo errno de la forma habitual. 4.11. DNS--Tú dices "whitehouse.gov", yo digo "198.137.240.92" Por si acaso no sabes qué es DNS, te diré que significa "Servicio de Nombres de Dominio" [Domain Name Service]. En una palabra, tú le dices cuál es la dirección de un sitio en forma humanamente legible y el te devuelve la dirección IP (para que puedas usarla con bind() , connect(), sendto(), o donde sea que la necesites). Así, cuando alguien escribe: $ telnet whitehouse.gov telnet puede averiguar que necesita conectarse (connect()) a "198.137.2.240.92" Pero, ¿cómo funciona? Tú vas a usar la función 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] Y estas son las descripciones de los campos de la estructura struct hostent: • h_name -- Nombre oficial de la máquina. • h_aliases -- Un vector terminado en NULL de nombres alternativos de máquina. • h_addrtype -- Tipo de la dirección que se devuelve; usualmente AF_INET. • h_length -- La longitud de la dirección en bytes. • h_addr_list -- Un vector terminado en cero de direcciones de red de la máquina. Las direcciones siguen la Ordenación de bytes de la red. • h_addr -- La primera dirección de h_addr_list. 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() más adelante). Pero, ¿cómo se usa? A veces (nos damos cuenta leyendo manuales de informática), no es suficiente con vomitarle la información al lector. En realidad, esta función es más fácil de usar de lo que parece. Aquí hay un programa de ejemplo : /* ** getip.c -- ejemplo de búsqueda 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) { // Comprobación de errores en la línea de comandos fprintf(stderr,"usage: getip address\n"); exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { // Obtener información 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 máquina ("whitehouse.gov") a gethostbyname() , y recuperas la información que te han devuelto en la estructura struct hostent. La única cosa rara que podrías encontrarte sería en el momento de imprimir la dirección 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. 5. Modelo Cliente-Servidor Amigo, este es un mundo cliente-servidor. Casi cualquier cosa en la red tiene que ver con procesos clientes que dialogan con procesos servidores y viceversa. Consideremos telnet, por ejemplo. Cuando conectas al puerto 23 de una máquina remota mediante telnet (el cliente) un programa de aquella máquina (llamado telnetd, el servidor) despierta a la vida. Gestiona la conexión telnet entrante, te presenta una pantalla de login , etc. Figura 2. Interacción Cliente-Servidor. El intercambio de información entre cliente y servidor se resume en la Figura 2 . Observa que el par cliente-servidor pueden hablar SOCK_STREAM , SOCK_DGRAM o cualquier otra cosa (siempre y cuando los dos hablen lo mismo). Algunos buenos ejemplos de parejas cliente-servidor son telnet/telnetd , ftp/ftpd, o bootp/bootpd. Cada vez que usas ftp, hay un programa remoto, ftpd, que te sirve. Con frecuencia, solamente habra un servidor en una máquina determinada, que atenderá a múltiples clientes usando fork(). El funcionamiento básico es: el servidor espera una conexión, la acepta (accept()) y usa fork() para obtener un proceso hijo que la atienda. Eso es lo que hace nuestro servidor de ejemplo en la siguiente sección. 5.1. Un servidor sencillo Todo lo que hace este servidor es enviar la cadena " Hello, World!\n" sobre una conexión de flujo. Todo lo que necesitas para probar este servidor es ejecutarlo en una ventana y atacarlo con telnet desde otra con: $ telnet remotehostname 3490 donde remotehostname es el nombre de la máquina donde estas ejecutando. El código servidor : (Nota: una barra invertida al final de una línea indica que esa línea se continúa en la siguiente.) /* ** server.c -- Ejemplo de servidor de sockets de flujo */ #include <stdio.h> de una vez int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; // información de la dirección de destino if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // obtener información de máquina perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // Ordenación de bytes de la máquina their_addr.sin_port = htons(PORT); // short, Ordenación de bytes de la red their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), 8); // poner a cero el resto de la estructura if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0; } Observa que si no ejecutas el servidor antes de llamar al cliente, connect() devuelve "Connection refused" (Conexión rechazada). Muy útil. 5.3. Sockets de datagramas En realidad no hay demasiado que contar aquí, así que sólo presentaré un par de programas de ejemplo: talker.c y listener.c. listener se sienta a esperar en la máquina hasta que llega un paquete al puerto 4950. talker envía un paquete a ese puerto en la máquina indicada que contiene lo que el usuario haya escrito en la línea de comandos. Este es el código fuente de listener.c : /* ** listener.c -- Ejemplo de servidor de sockets de datagramas */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MYPORT 4950 // puerto al que conectarán los clientes #define MAXBUFLEN 100 int main(void) { int sockfd; struct sockaddr_in my_addr; // información sobre mi dirección struct sockaddr_in their_addr; // información sobre la dirección del cliente int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; // Ordenación de bytes de máquina my_addr.sin_port = htons(MYPORT); // short, Ordenación de bytes de la red my_addr.sin_addr.s_addr = INADDR_ANY; // rellenar con mi dirección IP memset(&(my_addr.sin_zero), '\0', 8); // poner a cero el resto de la estructura if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr)); printf("packet is %d bytes long\n",numbytes); buf[numbytes] = '\0'; printf("packet contains \"%s\"\n",buf); close(sockfd); return 0; } Observa que en nuestra llamada a socket() finalmente estamos usando SOCK_DGRAM. Observa también que no hay necesidad de escuchar (listen()) o aceptar (accept()). ¡Esa es una de las ventajas de usar sockets de datagramas sin conexión! A continuación el código fuente de talker.c : /* ** talker.c -- ejemplo de cliente de datagramas */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define MYPORT 4950 // puerto donde vamos a conectarnos int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // información sobre la dirección del servidor struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // obtener información de máquina perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // Ordenación de bytes de máquina their_addr.sin_port = htons(MYPORT); // short, Ordenación de bytes de la red their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // poner a cero el resto de la estructura if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n", numbytes, inet_ntoa (their_addr.sin_addr)); close(sockfd); return 0; }
Docsity logo



Copyright © 2024 Ladybird Srl - Via Leonardo da Vinci 16, 10126, Torino, Italy - VAT 10816460017 - All rights reserved