Programacion de Socket en Linux

49
PROGRAMACIÓN DE SOCKETS CON C Y LINUX En los puntos 1 a 3 se cuentan de forma muy sencilla unos conceptos básicos sobre comunicaciones en red y los sockets y la arquitectura cliente/servidor. Si ya sabes de qué va el tema, te lo puedes saltar. De todas formas, no estaría de más un ojo al punto 2, ya que se introduce alguna cosa avanzadilla. Del 4 al 7 se cuentan por encima los pasos que deberían seguir nuestros programas cliente y servidor para conectarse y hablar entre ellos, las funciones de c/linux a las que deberían irse llamando, así como la configuración necesaria de los ficheros de linux implicados. Del 8 al 10 se muestra un código de ejemplo que funciona y se explica paso a paso. El 11 y 12 para introducirse en temas más complejos dentro de sockets. 1. Introducción 2. Los sockets 3. Arquitectura cliente/servidor 4. La conexión 5. El servidor 6. El cliente 7. Ficheros unix implicados 8. Ejemplo (Fuentes.zip ) 9. Código del servidor 10. Código del cliente 11. Algunas consideraciones 12. Bibliografia Temas algo más avanzados: una mini-librería para facilitar el uso de sockets. Atender a varios clientes utilizando select() . INTRODUCCIÓN En una red de ordenadores hay varios ordenadores que están conectados entre si por un cable. A través de dicho cable pueden transmitirse información. Es claro que deben estar de acuerdo en cómo transmitir esa información, de forma que cualquiera de ellos pueda entender lo que están transmitiendo los otros, de la misma forma que nosotros nos ponemos de acuerdo para hablar en inglés cuando uno es italiano, el otro francés, el otro español y el otro alemán. Al "idioma" que utilizan los ordenadores para comunicarse cuando están en red se le denomina protocolo. Hay muchísimos protocolos de comunicación, entre los cuales el más extendido es el TCP/IP. El más extendido porque es el que se utiliza en Internet. Aunque todo esto pueda parecer complicado y que no podemos hacer mucho con ello, lo cierto es que podemos aprovecharlo para comunicar dos programas nuestros que

description

Programacion de Socket en Linux en lenguaje C, muy completo

Transcript of Programacion de Socket en Linux

Page 1: Programacion de Socket en Linux

PROGRAMACIOacuteN DE SOCKETS CON C Y LINUX

En los puntos 1 a 3 se cuentan de forma muy sencilla unos conceptos baacutesicos sobre comunicaciones en red y los sockets y la arquitectura clienteservidor Si ya sabes de queacute va el tema te lo puedes saltar De todas formas no estariacutea de maacutes un ojo al punto 2 ya que se introduce alguna cosa avanzadilla

Del 4 al 7 se cuentan por encima los pasos que deberiacutean seguir nuestros programas cliente y servidor para conectarse y hablar entre ellos las funciones de clinux a las que deberiacutean irse llamando asiacute como la configuracioacuten necesaria de los ficheros de linux implicados

Del 8 al 10 se muestra un coacutedigo de ejemplo que funciona y se explica paso a paso

El 11 y 12 para introducirse en temas maacutes complejos dentro de sockets

1 Introduccioacuten 2 Los sockets 3 Arquitectura clienteservidor 4 La conexioacuten 5 El servidor 6 El cliente 7 Ficheros unix implicados 8 Ejemplo (Fuenteszip) 9 Coacutedigo del servidor 10 Coacutedigo del cliente 11 Algunas consideraciones 12 Bibliografia

Temas algo maacutes avanzados una mini-libreriacutea para facilitar el uso de sockets Atender a varios clientes utilizando select()

INTRODUCCIOacuteN

En una red de ordenadores hay varios ordenadores que estaacuten conectados entre si por un cable A traveacutes de dicho cable pueden transmitirse informacioacuten Es claro que deben estar de acuerdo en coacutemo transmitir esa informacioacuten de forma que cualquiera de ellos pueda entender lo que estaacuten transmitiendo los otros de la misma forma que nosotros nos ponemos de acuerdo para hablar en ingleacutes cuando uno es italiano el otro franceacutes el otro espantildeol y el otro alemaacuten

Al idioma que utilizan los ordenadores para comunicarse cuando estaacuten en red se le denomina protocolo Hay muchiacutesimos protocolos de comunicacioacuten entre los cuales el maacutes extendido es el TCPIP El maacutes extendido porque es el que se utiliza en Internet

Aunque todo esto pueda parecer complicado y que no podemos hacer mucho con ello lo cierto es que podemos aprovecharlo para comunicar dos programas nuestros que

esteacuten corriendo en ordenadores distintos De hecho con C en LinuxUnix tenemos una serie de funciones que nos permiten enviar y recibir datos de otros programas en C o en otros lenguajes de programacioacuten que esteacuten corriendo en otros ordenadores de la misma red

En este artiacuteculo no se pretende dar una descripcioacuten detallada y rigurosa del protocolo TCPIP y lo que va alrededor de eacutel Simplemente se daraacuten unas nociones baacutesicas de manera informal con la intencioacuten de que se pueda comprender un pequentildeo ejemplo de programacioacuten en C

Estaacute por tanto orientado a personas que tengan unos conocimientos baacutesicos de C en Linux y deseen o necesiten comunicar dos programas en C que corren simultaacuteneamente en dos ordenadores distintos conectados en red El ejemplo propuesto puede servir como guiacutea inicial que se pude complicar todo lo que se desee

LOS SOCKETS

Una forma de conseguir que dos programas se transmitan datos basada en el protocolo TCPIP es la programacioacuten de sockets Un socket no es maacutes que un canal de comunicacioacuten entre dos programas que corren sobre ordenadores distintos o incluso en el mismo ordenador

Desde el punto de vista de programacioacuten un socket no es maacutes que un fichero que se abre de una manera especial Una vez abierto se pueden escribir y leer datos de eacutel con las habituales funciones de read() y write() del lenguaje C Hablaremos de todo esto con detalle maacutes adelante

Existen baacutesicamente dos tipos de canales de comunicacioacuten o sockets los orientados a conexioacuten y los no orientados a conexioacuten

En el primer caso ambos programas deben conectarse entre ellos con un socket y hasta que no esteacute establecida correctamente la conexioacuten ninguno de los dos puede transmitir datos Esta es la parte TCP del protocolo TCPIP y garantiza que todos los datos van a llegar de un programa al otro correctamente Se utiliza cuando la informacioacuten a transmitir es importante no se puede perder ninguacuten dato y no importa que los programas se queden bloqueados esperando o transmitiendo datos Si uno de los programas estaacute atareado en otra cosa y no atiende la comunicacioacuten el otro quedaraacute bloqueado hasta que el primero lea o escriba los datos

En el segundo caso no es necesario que los programas se conecten Cualquiera de ellos puede transmitir datos en cualquier momento independientemente de que el otro programa esteacute escuchando o no Es el llamado protocolo UDP y garantiza que los datos que lleguen son correctos pero no garantiza que lleguen todos Se utiliza cuando es muy importante que el programa no se quede bloqueado y no importa que se pierdan datos Imaginemos por ejemplo un programa que estaacute controlando la temperatura de un horno y enviacutea dicha temperatura a un ordenador en una sala de control para que eacuteste presente unos graacuteficos de temperatura Obviamente es maacutes importante el control del horno que el perfecto refresco de los graacuteficos

En el ejemplo y a partir de este momento nos referimos uacutenicamente a sockets TCP Los UDP son baacutesicamente iguales aunque hay pequentildeas diferencias en la forma de abrirlos

ARQUITECTURA CLIENTE SERVIDOR

A la hora de comunicar dos programas existen varias posibilidades para establecer la conexioacuten inicialmente Una de ellas es la utilizada aquiacute Uno de los programas debe estar arrancado y en espera de que otro quiera conectarse a eacutel Nunca da el primer paso en la conexioacuten Al programa que actuacutea de esta forma se le conoce como servidor Su nombre se debe a que normalmente es el que tiene la informacioacuten que sea disponible y la sirve al que se la pida Por ejemplo el servidor de paacuteginas web tiene las paacuteginas web y se las enviacutea al navegador que se lo solcite

El otro programa es el que da el primer paso En el momento de arrancarlo o cuando lo necesite intenta conectarse al servidor Este programa se denomina cliente Su nombre se debe a que es el que solicita informacioacuten al servidor El navegador de Internet pide la paacutegina web al servidor de Internet

En este ejemplo el servidor de paacuteginas web se llama servidor porque estaacute (o deberiacutea de estar) siempre encendido y pendiente de que alguien se conecte a eacutel y le pida una paacutegina El navegador de Internet es el cliente puesto que se arranca cuando nosotros lo arrancamos y solicita conexioacuten con el servidor cuando nosotros escribimos por ejemplo wwwgooglecom

En el juego del Quake debe haber un servidor que es el que tiene el escenario del juego y la situacioacuten de todos los jugadores en eacutel Cuando un nuevo jugador arranca el juego en su ordenador se conecta al servidor y le pide el escenario del juego para presentarlo en la pantalla Los movimientos que realiza el jugador se transmiten al servidor y este actualiza escenarios a todos los jugadores

Resumiendo servidor es el programa que permanece pasivo a la espera de que alguien solicite conexioacuten con eacutel normalmente para pedirle alguacuten dato Cliente es el programa que solicita la conexioacuten para normalmente pedir datos al servidor

LA CONEXIOacuteN

Para poder realizar la conexioacuten entre ambos programas son necesarias varias cosas

bull Direccioacuten IP del servidor

Cada ordenador de una red tiene asignado un nuacutemero uacutenico que sirve para identificarle y distinguirle de los demaacutes de forma que cuando un ordenador quiere hablar con otro manda la informacioacuten a dicho nuacutemero Es similar a nuestros nuacutemeros de teleacutefono Si quiero hablar con mi amigo Josechu primero marco su nuacutemero de teleacutefono y luego hablo con eacutel

El servidor no necesita la direccioacuten de ninguno de los dos ordenadores al igual que nosotros para recibir una llamada por teleacutefono no necesitamos saber el nuacutemero de nadie ni siquiera el nuestro El cliente siacute necesita saber el nuacutemero del servidor al igual que nosotros para llamar a alguien necesitamos saber su nuacutemero de teleacutefono

La direccioacuten IP es un nuacutemero del estilo 192100234 iexclTodos lo hemos visto en Internet En resumidas cuentas el cliente debe conocer a queacute ordenador desea conectarse En nuestro navegador de Internet facilitamos la direccioacuten IP del servidor al que queremos conectarnos a traveacutes de su nombre (wwwgooglecom) Obviamente este nombre hay que traducirlo a una direccioacuten IP pero nuestro navegador e Internet se encargan de eso por nosotros

bull Servicio que queremos crear utilizar

Si llamamos a una empresa puede haber en ella muchas personas cada una con su extensioacuten de teleacutefono propia Normalmente la persona en concreto con la que hablemos no da igual lo que queremos es alguien que nos atienda y nos de un determinado servicio como recoger una queja darnos una informacioacuten tomarnos nota de un pedido etc

De la misma forma en un mismo ordenador pueden estar corriendo varios programas servidores cada uno de ellos dando un servicio distinto Por ejemplo un ordenador puede tener un servidor de Quake y un servidor de paacuteginas web corriendo a la vez

Cuando un cliente desea conectarse debe indicar queacute servicio quiere igual que al llamar a la empresa necesitamos decir la extensioacuten de la persona con la que queremos hablar o al menos decir su nombre para que la telefonista nos ponga con la persona adecuada

Por ello cada servicio dentro del ordenador debe tener un nuacutemero uacutenico que lo identifique (como la extensioacuten de teleacutefono) Estos nuacutemeros son enteros normales y van de 1 a 65535 Los nuacutemero bajos desde 1 a 1023 estaacuten reservados para servicios habituales de los sistemas operativos (www ftp mail ping etc) El resto estaacuten a disposicioacuten del programador y sirven para cosas como Quake

Tanto el servidor como el cliente deben conocer el nuacutemero del servicio al que atienden o se conectan ya que el sistema operativo es maacutes torpe que la telefonista El sistema operativo pediraacute al cliente la extensioacuten (no le vale el nombre del servicio) y cantaraacute el nuacutemero en voz alta para ver queacute persona de la empresa atiende a ese nuacutemero Tanto el que llama como el que le debe atender se tienen que saber el nuacutemero

En el caso del navegador de Internet estamos indicando el servicio con la www en wwwgooglecom servicio de paacuteginas web Tambieacuten es posible por ejemplo ftpgooglecom si googlecom admite clientes ftp Nuestro ordenador es lo suficientemente listo como para saber a queacute nuacutemero corresponden esos servicios habituales

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programacioacuten en C de los sockets Si no tienes unos miacutenimos conocimientos de C es mejor que los adquieras antes de seguir

Con C en UnixLinux los pasos que debe seguir un programa servidor son los siguientes

bull Apertura de un socket mediante la funcioacuten socket() Esta funcioacuten devuelve un descriptor de fichero normal como puede devolverlo open() La funcioacuten socket() no hace absolutamente nada salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociaraacute a una conexioacuten en red

bull Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket Se consigue mediante la funcioacuten bind() El sistema todaviacutea no atenderaacute a las conexiones de clientes simplemente anota que cuando empiece a hacerlo tendraacute que avisarnos a nosotros Es en esta llamada cuando se debe indicar el nuacutemero de servicio al que se quiere atender

bull Avisar al sistema de que comience a atender dicha conexioacuten de red Se consigue mediante la funcioacuten listen() A partir de este momento el sistema operativo anotaraacute la conexioacuten de cualquier cliente para pasaacuternosla cuando se lo pidamos Si llegan clientes maacutes raacutepido de lo que somos capaces de atenderlos el sistema operativo hace una cola con ellos y nos los iraacute pasando seguacuten vayamos pidieacutendolo

bull Pedir y aceptar las conexiones de clientes al sistema operativo Para ello hacemos una llamada a la funcioacuten accept() Esta funcioacuten le indica al sistema operativo que nos deacute al siguiente cliente de la cola Si no hay clientes se quedaraacute bloqueada hasta que alguacuten cliente se conecte

bull Escribir y recibir datos del cliente por medio de las funciones write() y read() que son exactamente las mismas que usamos para escribir o leer de un fichero Obviamente tanto cliente como servidor deben saber queacute datos esperan recibir queacute datos deben enviar y en queacute formato Puedes ver coacutemo se pueden poner de acuerdo en estos mensajes en el apartado de mensajes

bull Cierre de la comunicacioacuten y del socket por medio de la funcioacuten close() que es la misma que sirve para cerrar un fichero

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes

bull Apertura de un socket como el servidor por medio de la funcioacuten socket()

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 2: Programacion de Socket en Linux

esteacuten corriendo en ordenadores distintos De hecho con C en LinuxUnix tenemos una serie de funciones que nos permiten enviar y recibir datos de otros programas en C o en otros lenguajes de programacioacuten que esteacuten corriendo en otros ordenadores de la misma red

En este artiacuteculo no se pretende dar una descripcioacuten detallada y rigurosa del protocolo TCPIP y lo que va alrededor de eacutel Simplemente se daraacuten unas nociones baacutesicas de manera informal con la intencioacuten de que se pueda comprender un pequentildeo ejemplo de programacioacuten en C

Estaacute por tanto orientado a personas que tengan unos conocimientos baacutesicos de C en Linux y deseen o necesiten comunicar dos programas en C que corren simultaacuteneamente en dos ordenadores distintos conectados en red El ejemplo propuesto puede servir como guiacutea inicial que se pude complicar todo lo que se desee

LOS SOCKETS

Una forma de conseguir que dos programas se transmitan datos basada en el protocolo TCPIP es la programacioacuten de sockets Un socket no es maacutes que un canal de comunicacioacuten entre dos programas que corren sobre ordenadores distintos o incluso en el mismo ordenador

Desde el punto de vista de programacioacuten un socket no es maacutes que un fichero que se abre de una manera especial Una vez abierto se pueden escribir y leer datos de eacutel con las habituales funciones de read() y write() del lenguaje C Hablaremos de todo esto con detalle maacutes adelante

Existen baacutesicamente dos tipos de canales de comunicacioacuten o sockets los orientados a conexioacuten y los no orientados a conexioacuten

En el primer caso ambos programas deben conectarse entre ellos con un socket y hasta que no esteacute establecida correctamente la conexioacuten ninguno de los dos puede transmitir datos Esta es la parte TCP del protocolo TCPIP y garantiza que todos los datos van a llegar de un programa al otro correctamente Se utiliza cuando la informacioacuten a transmitir es importante no se puede perder ninguacuten dato y no importa que los programas se queden bloqueados esperando o transmitiendo datos Si uno de los programas estaacute atareado en otra cosa y no atiende la comunicacioacuten el otro quedaraacute bloqueado hasta que el primero lea o escriba los datos

En el segundo caso no es necesario que los programas se conecten Cualquiera de ellos puede transmitir datos en cualquier momento independientemente de que el otro programa esteacute escuchando o no Es el llamado protocolo UDP y garantiza que los datos que lleguen son correctos pero no garantiza que lleguen todos Se utiliza cuando es muy importante que el programa no se quede bloqueado y no importa que se pierdan datos Imaginemos por ejemplo un programa que estaacute controlando la temperatura de un horno y enviacutea dicha temperatura a un ordenador en una sala de control para que eacuteste presente unos graacuteficos de temperatura Obviamente es maacutes importante el control del horno que el perfecto refresco de los graacuteficos

En el ejemplo y a partir de este momento nos referimos uacutenicamente a sockets TCP Los UDP son baacutesicamente iguales aunque hay pequentildeas diferencias en la forma de abrirlos

ARQUITECTURA CLIENTE SERVIDOR

A la hora de comunicar dos programas existen varias posibilidades para establecer la conexioacuten inicialmente Una de ellas es la utilizada aquiacute Uno de los programas debe estar arrancado y en espera de que otro quiera conectarse a eacutel Nunca da el primer paso en la conexioacuten Al programa que actuacutea de esta forma se le conoce como servidor Su nombre se debe a que normalmente es el que tiene la informacioacuten que sea disponible y la sirve al que se la pida Por ejemplo el servidor de paacuteginas web tiene las paacuteginas web y se las enviacutea al navegador que se lo solcite

El otro programa es el que da el primer paso En el momento de arrancarlo o cuando lo necesite intenta conectarse al servidor Este programa se denomina cliente Su nombre se debe a que es el que solicita informacioacuten al servidor El navegador de Internet pide la paacutegina web al servidor de Internet

En este ejemplo el servidor de paacuteginas web se llama servidor porque estaacute (o deberiacutea de estar) siempre encendido y pendiente de que alguien se conecte a eacutel y le pida una paacutegina El navegador de Internet es el cliente puesto que se arranca cuando nosotros lo arrancamos y solicita conexioacuten con el servidor cuando nosotros escribimos por ejemplo wwwgooglecom

En el juego del Quake debe haber un servidor que es el que tiene el escenario del juego y la situacioacuten de todos los jugadores en eacutel Cuando un nuevo jugador arranca el juego en su ordenador se conecta al servidor y le pide el escenario del juego para presentarlo en la pantalla Los movimientos que realiza el jugador se transmiten al servidor y este actualiza escenarios a todos los jugadores

Resumiendo servidor es el programa que permanece pasivo a la espera de que alguien solicite conexioacuten con eacutel normalmente para pedirle alguacuten dato Cliente es el programa que solicita la conexioacuten para normalmente pedir datos al servidor

LA CONEXIOacuteN

Para poder realizar la conexioacuten entre ambos programas son necesarias varias cosas

bull Direccioacuten IP del servidor

Cada ordenador de una red tiene asignado un nuacutemero uacutenico que sirve para identificarle y distinguirle de los demaacutes de forma que cuando un ordenador quiere hablar con otro manda la informacioacuten a dicho nuacutemero Es similar a nuestros nuacutemeros de teleacutefono Si quiero hablar con mi amigo Josechu primero marco su nuacutemero de teleacutefono y luego hablo con eacutel

El servidor no necesita la direccioacuten de ninguno de los dos ordenadores al igual que nosotros para recibir una llamada por teleacutefono no necesitamos saber el nuacutemero de nadie ni siquiera el nuestro El cliente siacute necesita saber el nuacutemero del servidor al igual que nosotros para llamar a alguien necesitamos saber su nuacutemero de teleacutefono

La direccioacuten IP es un nuacutemero del estilo 192100234 iexclTodos lo hemos visto en Internet En resumidas cuentas el cliente debe conocer a queacute ordenador desea conectarse En nuestro navegador de Internet facilitamos la direccioacuten IP del servidor al que queremos conectarnos a traveacutes de su nombre (wwwgooglecom) Obviamente este nombre hay que traducirlo a una direccioacuten IP pero nuestro navegador e Internet se encargan de eso por nosotros

bull Servicio que queremos crear utilizar

Si llamamos a una empresa puede haber en ella muchas personas cada una con su extensioacuten de teleacutefono propia Normalmente la persona en concreto con la que hablemos no da igual lo que queremos es alguien que nos atienda y nos de un determinado servicio como recoger una queja darnos una informacioacuten tomarnos nota de un pedido etc

De la misma forma en un mismo ordenador pueden estar corriendo varios programas servidores cada uno de ellos dando un servicio distinto Por ejemplo un ordenador puede tener un servidor de Quake y un servidor de paacuteginas web corriendo a la vez

Cuando un cliente desea conectarse debe indicar queacute servicio quiere igual que al llamar a la empresa necesitamos decir la extensioacuten de la persona con la que queremos hablar o al menos decir su nombre para que la telefonista nos ponga con la persona adecuada

Por ello cada servicio dentro del ordenador debe tener un nuacutemero uacutenico que lo identifique (como la extensioacuten de teleacutefono) Estos nuacutemeros son enteros normales y van de 1 a 65535 Los nuacutemero bajos desde 1 a 1023 estaacuten reservados para servicios habituales de los sistemas operativos (www ftp mail ping etc) El resto estaacuten a disposicioacuten del programador y sirven para cosas como Quake

Tanto el servidor como el cliente deben conocer el nuacutemero del servicio al que atienden o se conectan ya que el sistema operativo es maacutes torpe que la telefonista El sistema operativo pediraacute al cliente la extensioacuten (no le vale el nombre del servicio) y cantaraacute el nuacutemero en voz alta para ver queacute persona de la empresa atiende a ese nuacutemero Tanto el que llama como el que le debe atender se tienen que saber el nuacutemero

En el caso del navegador de Internet estamos indicando el servicio con la www en wwwgooglecom servicio de paacuteginas web Tambieacuten es posible por ejemplo ftpgooglecom si googlecom admite clientes ftp Nuestro ordenador es lo suficientemente listo como para saber a queacute nuacutemero corresponden esos servicios habituales

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programacioacuten en C de los sockets Si no tienes unos miacutenimos conocimientos de C es mejor que los adquieras antes de seguir

Con C en UnixLinux los pasos que debe seguir un programa servidor son los siguientes

bull Apertura de un socket mediante la funcioacuten socket() Esta funcioacuten devuelve un descriptor de fichero normal como puede devolverlo open() La funcioacuten socket() no hace absolutamente nada salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociaraacute a una conexioacuten en red

bull Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket Se consigue mediante la funcioacuten bind() El sistema todaviacutea no atenderaacute a las conexiones de clientes simplemente anota que cuando empiece a hacerlo tendraacute que avisarnos a nosotros Es en esta llamada cuando se debe indicar el nuacutemero de servicio al que se quiere atender

bull Avisar al sistema de que comience a atender dicha conexioacuten de red Se consigue mediante la funcioacuten listen() A partir de este momento el sistema operativo anotaraacute la conexioacuten de cualquier cliente para pasaacuternosla cuando se lo pidamos Si llegan clientes maacutes raacutepido de lo que somos capaces de atenderlos el sistema operativo hace una cola con ellos y nos los iraacute pasando seguacuten vayamos pidieacutendolo

bull Pedir y aceptar las conexiones de clientes al sistema operativo Para ello hacemos una llamada a la funcioacuten accept() Esta funcioacuten le indica al sistema operativo que nos deacute al siguiente cliente de la cola Si no hay clientes se quedaraacute bloqueada hasta que alguacuten cliente se conecte

bull Escribir y recibir datos del cliente por medio de las funciones write() y read() que son exactamente las mismas que usamos para escribir o leer de un fichero Obviamente tanto cliente como servidor deben saber queacute datos esperan recibir queacute datos deben enviar y en queacute formato Puedes ver coacutemo se pueden poner de acuerdo en estos mensajes en el apartado de mensajes

bull Cierre de la comunicacioacuten y del socket por medio de la funcioacuten close() que es la misma que sirve para cerrar un fichero

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes

bull Apertura de un socket como el servidor por medio de la funcioacuten socket()

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 3: Programacion de Socket en Linux

En el ejemplo y a partir de este momento nos referimos uacutenicamente a sockets TCP Los UDP son baacutesicamente iguales aunque hay pequentildeas diferencias en la forma de abrirlos

ARQUITECTURA CLIENTE SERVIDOR

A la hora de comunicar dos programas existen varias posibilidades para establecer la conexioacuten inicialmente Una de ellas es la utilizada aquiacute Uno de los programas debe estar arrancado y en espera de que otro quiera conectarse a eacutel Nunca da el primer paso en la conexioacuten Al programa que actuacutea de esta forma se le conoce como servidor Su nombre se debe a que normalmente es el que tiene la informacioacuten que sea disponible y la sirve al que se la pida Por ejemplo el servidor de paacuteginas web tiene las paacuteginas web y se las enviacutea al navegador que se lo solcite

El otro programa es el que da el primer paso En el momento de arrancarlo o cuando lo necesite intenta conectarse al servidor Este programa se denomina cliente Su nombre se debe a que es el que solicita informacioacuten al servidor El navegador de Internet pide la paacutegina web al servidor de Internet

En este ejemplo el servidor de paacuteginas web se llama servidor porque estaacute (o deberiacutea de estar) siempre encendido y pendiente de que alguien se conecte a eacutel y le pida una paacutegina El navegador de Internet es el cliente puesto que se arranca cuando nosotros lo arrancamos y solicita conexioacuten con el servidor cuando nosotros escribimos por ejemplo wwwgooglecom

En el juego del Quake debe haber un servidor que es el que tiene el escenario del juego y la situacioacuten de todos los jugadores en eacutel Cuando un nuevo jugador arranca el juego en su ordenador se conecta al servidor y le pide el escenario del juego para presentarlo en la pantalla Los movimientos que realiza el jugador se transmiten al servidor y este actualiza escenarios a todos los jugadores

Resumiendo servidor es el programa que permanece pasivo a la espera de que alguien solicite conexioacuten con eacutel normalmente para pedirle alguacuten dato Cliente es el programa que solicita la conexioacuten para normalmente pedir datos al servidor

LA CONEXIOacuteN

Para poder realizar la conexioacuten entre ambos programas son necesarias varias cosas

bull Direccioacuten IP del servidor

Cada ordenador de una red tiene asignado un nuacutemero uacutenico que sirve para identificarle y distinguirle de los demaacutes de forma que cuando un ordenador quiere hablar con otro manda la informacioacuten a dicho nuacutemero Es similar a nuestros nuacutemeros de teleacutefono Si quiero hablar con mi amigo Josechu primero marco su nuacutemero de teleacutefono y luego hablo con eacutel

El servidor no necesita la direccioacuten de ninguno de los dos ordenadores al igual que nosotros para recibir una llamada por teleacutefono no necesitamos saber el nuacutemero de nadie ni siquiera el nuestro El cliente siacute necesita saber el nuacutemero del servidor al igual que nosotros para llamar a alguien necesitamos saber su nuacutemero de teleacutefono

La direccioacuten IP es un nuacutemero del estilo 192100234 iexclTodos lo hemos visto en Internet En resumidas cuentas el cliente debe conocer a queacute ordenador desea conectarse En nuestro navegador de Internet facilitamos la direccioacuten IP del servidor al que queremos conectarnos a traveacutes de su nombre (wwwgooglecom) Obviamente este nombre hay que traducirlo a una direccioacuten IP pero nuestro navegador e Internet se encargan de eso por nosotros

bull Servicio que queremos crear utilizar

Si llamamos a una empresa puede haber en ella muchas personas cada una con su extensioacuten de teleacutefono propia Normalmente la persona en concreto con la que hablemos no da igual lo que queremos es alguien que nos atienda y nos de un determinado servicio como recoger una queja darnos una informacioacuten tomarnos nota de un pedido etc

De la misma forma en un mismo ordenador pueden estar corriendo varios programas servidores cada uno de ellos dando un servicio distinto Por ejemplo un ordenador puede tener un servidor de Quake y un servidor de paacuteginas web corriendo a la vez

Cuando un cliente desea conectarse debe indicar queacute servicio quiere igual que al llamar a la empresa necesitamos decir la extensioacuten de la persona con la que queremos hablar o al menos decir su nombre para que la telefonista nos ponga con la persona adecuada

Por ello cada servicio dentro del ordenador debe tener un nuacutemero uacutenico que lo identifique (como la extensioacuten de teleacutefono) Estos nuacutemeros son enteros normales y van de 1 a 65535 Los nuacutemero bajos desde 1 a 1023 estaacuten reservados para servicios habituales de los sistemas operativos (www ftp mail ping etc) El resto estaacuten a disposicioacuten del programador y sirven para cosas como Quake

Tanto el servidor como el cliente deben conocer el nuacutemero del servicio al que atienden o se conectan ya que el sistema operativo es maacutes torpe que la telefonista El sistema operativo pediraacute al cliente la extensioacuten (no le vale el nombre del servicio) y cantaraacute el nuacutemero en voz alta para ver queacute persona de la empresa atiende a ese nuacutemero Tanto el que llama como el que le debe atender se tienen que saber el nuacutemero

En el caso del navegador de Internet estamos indicando el servicio con la www en wwwgooglecom servicio de paacuteginas web Tambieacuten es posible por ejemplo ftpgooglecom si googlecom admite clientes ftp Nuestro ordenador es lo suficientemente listo como para saber a queacute nuacutemero corresponden esos servicios habituales

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programacioacuten en C de los sockets Si no tienes unos miacutenimos conocimientos de C es mejor que los adquieras antes de seguir

Con C en UnixLinux los pasos que debe seguir un programa servidor son los siguientes

bull Apertura de un socket mediante la funcioacuten socket() Esta funcioacuten devuelve un descriptor de fichero normal como puede devolverlo open() La funcioacuten socket() no hace absolutamente nada salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociaraacute a una conexioacuten en red

bull Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket Se consigue mediante la funcioacuten bind() El sistema todaviacutea no atenderaacute a las conexiones de clientes simplemente anota que cuando empiece a hacerlo tendraacute que avisarnos a nosotros Es en esta llamada cuando se debe indicar el nuacutemero de servicio al que se quiere atender

bull Avisar al sistema de que comience a atender dicha conexioacuten de red Se consigue mediante la funcioacuten listen() A partir de este momento el sistema operativo anotaraacute la conexioacuten de cualquier cliente para pasaacuternosla cuando se lo pidamos Si llegan clientes maacutes raacutepido de lo que somos capaces de atenderlos el sistema operativo hace una cola con ellos y nos los iraacute pasando seguacuten vayamos pidieacutendolo

bull Pedir y aceptar las conexiones de clientes al sistema operativo Para ello hacemos una llamada a la funcioacuten accept() Esta funcioacuten le indica al sistema operativo que nos deacute al siguiente cliente de la cola Si no hay clientes se quedaraacute bloqueada hasta que alguacuten cliente se conecte

bull Escribir y recibir datos del cliente por medio de las funciones write() y read() que son exactamente las mismas que usamos para escribir o leer de un fichero Obviamente tanto cliente como servidor deben saber queacute datos esperan recibir queacute datos deben enviar y en queacute formato Puedes ver coacutemo se pueden poner de acuerdo en estos mensajes en el apartado de mensajes

bull Cierre de la comunicacioacuten y del socket por medio de la funcioacuten close() que es la misma que sirve para cerrar un fichero

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes

bull Apertura de un socket como el servidor por medio de la funcioacuten socket()

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 4: Programacion de Socket en Linux

Cada ordenador de una red tiene asignado un nuacutemero uacutenico que sirve para identificarle y distinguirle de los demaacutes de forma que cuando un ordenador quiere hablar con otro manda la informacioacuten a dicho nuacutemero Es similar a nuestros nuacutemeros de teleacutefono Si quiero hablar con mi amigo Josechu primero marco su nuacutemero de teleacutefono y luego hablo con eacutel

El servidor no necesita la direccioacuten de ninguno de los dos ordenadores al igual que nosotros para recibir una llamada por teleacutefono no necesitamos saber el nuacutemero de nadie ni siquiera el nuestro El cliente siacute necesita saber el nuacutemero del servidor al igual que nosotros para llamar a alguien necesitamos saber su nuacutemero de teleacutefono

La direccioacuten IP es un nuacutemero del estilo 192100234 iexclTodos lo hemos visto en Internet En resumidas cuentas el cliente debe conocer a queacute ordenador desea conectarse En nuestro navegador de Internet facilitamos la direccioacuten IP del servidor al que queremos conectarnos a traveacutes de su nombre (wwwgooglecom) Obviamente este nombre hay que traducirlo a una direccioacuten IP pero nuestro navegador e Internet se encargan de eso por nosotros

bull Servicio que queremos crear utilizar

Si llamamos a una empresa puede haber en ella muchas personas cada una con su extensioacuten de teleacutefono propia Normalmente la persona en concreto con la que hablemos no da igual lo que queremos es alguien que nos atienda y nos de un determinado servicio como recoger una queja darnos una informacioacuten tomarnos nota de un pedido etc

De la misma forma en un mismo ordenador pueden estar corriendo varios programas servidores cada uno de ellos dando un servicio distinto Por ejemplo un ordenador puede tener un servidor de Quake y un servidor de paacuteginas web corriendo a la vez

Cuando un cliente desea conectarse debe indicar queacute servicio quiere igual que al llamar a la empresa necesitamos decir la extensioacuten de la persona con la que queremos hablar o al menos decir su nombre para que la telefonista nos ponga con la persona adecuada

Por ello cada servicio dentro del ordenador debe tener un nuacutemero uacutenico que lo identifique (como la extensioacuten de teleacutefono) Estos nuacutemeros son enteros normales y van de 1 a 65535 Los nuacutemero bajos desde 1 a 1023 estaacuten reservados para servicios habituales de los sistemas operativos (www ftp mail ping etc) El resto estaacuten a disposicioacuten del programador y sirven para cosas como Quake

Tanto el servidor como el cliente deben conocer el nuacutemero del servicio al que atienden o se conectan ya que el sistema operativo es maacutes torpe que la telefonista El sistema operativo pediraacute al cliente la extensioacuten (no le vale el nombre del servicio) y cantaraacute el nuacutemero en voz alta para ver queacute persona de la empresa atiende a ese nuacutemero Tanto el que llama como el que le debe atender se tienen que saber el nuacutemero

En el caso del navegador de Internet estamos indicando el servicio con la www en wwwgooglecom servicio de paacuteginas web Tambieacuten es posible por ejemplo ftpgooglecom si googlecom admite clientes ftp Nuestro ordenador es lo suficientemente listo como para saber a queacute nuacutemero corresponden esos servicios habituales

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programacioacuten en C de los sockets Si no tienes unos miacutenimos conocimientos de C es mejor que los adquieras antes de seguir

Con C en UnixLinux los pasos que debe seguir un programa servidor son los siguientes

bull Apertura de un socket mediante la funcioacuten socket() Esta funcioacuten devuelve un descriptor de fichero normal como puede devolverlo open() La funcioacuten socket() no hace absolutamente nada salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociaraacute a una conexioacuten en red

bull Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket Se consigue mediante la funcioacuten bind() El sistema todaviacutea no atenderaacute a las conexiones de clientes simplemente anota que cuando empiece a hacerlo tendraacute que avisarnos a nosotros Es en esta llamada cuando se debe indicar el nuacutemero de servicio al que se quiere atender

bull Avisar al sistema de que comience a atender dicha conexioacuten de red Se consigue mediante la funcioacuten listen() A partir de este momento el sistema operativo anotaraacute la conexioacuten de cualquier cliente para pasaacuternosla cuando se lo pidamos Si llegan clientes maacutes raacutepido de lo que somos capaces de atenderlos el sistema operativo hace una cola con ellos y nos los iraacute pasando seguacuten vayamos pidieacutendolo

bull Pedir y aceptar las conexiones de clientes al sistema operativo Para ello hacemos una llamada a la funcioacuten accept() Esta funcioacuten le indica al sistema operativo que nos deacute al siguiente cliente de la cola Si no hay clientes se quedaraacute bloqueada hasta que alguacuten cliente se conecte

bull Escribir y recibir datos del cliente por medio de las funciones write() y read() que son exactamente las mismas que usamos para escribir o leer de un fichero Obviamente tanto cliente como servidor deben saber queacute datos esperan recibir queacute datos deben enviar y en queacute formato Puedes ver coacutemo se pueden poner de acuerdo en estos mensajes en el apartado de mensajes

bull Cierre de la comunicacioacuten y del socket por medio de la funcioacuten close() que es la misma que sirve para cerrar un fichero

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes

bull Apertura de un socket como el servidor por medio de la funcioacuten socket()

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 5: Programacion de Socket en Linux

En el caso del navegador de Internet estamos indicando el servicio con la www en wwwgooglecom servicio de paacuteginas web Tambieacuten es posible por ejemplo ftpgooglecom si googlecom admite clientes ftp Nuestro ordenador es lo suficientemente listo como para saber a queacute nuacutemero corresponden esos servicios habituales

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programacioacuten en C de los sockets Si no tienes unos miacutenimos conocimientos de C es mejor que los adquieras antes de seguir

Con C en UnixLinux los pasos que debe seguir un programa servidor son los siguientes

bull Apertura de un socket mediante la funcioacuten socket() Esta funcioacuten devuelve un descriptor de fichero normal como puede devolverlo open() La funcioacuten socket() no hace absolutamente nada salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociaraacute a una conexioacuten en red

bull Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket Se consigue mediante la funcioacuten bind() El sistema todaviacutea no atenderaacute a las conexiones de clientes simplemente anota que cuando empiece a hacerlo tendraacute que avisarnos a nosotros Es en esta llamada cuando se debe indicar el nuacutemero de servicio al que se quiere atender

bull Avisar al sistema de que comience a atender dicha conexioacuten de red Se consigue mediante la funcioacuten listen() A partir de este momento el sistema operativo anotaraacute la conexioacuten de cualquier cliente para pasaacuternosla cuando se lo pidamos Si llegan clientes maacutes raacutepido de lo que somos capaces de atenderlos el sistema operativo hace una cola con ellos y nos los iraacute pasando seguacuten vayamos pidieacutendolo

bull Pedir y aceptar las conexiones de clientes al sistema operativo Para ello hacemos una llamada a la funcioacuten accept() Esta funcioacuten le indica al sistema operativo que nos deacute al siguiente cliente de la cola Si no hay clientes se quedaraacute bloqueada hasta que alguacuten cliente se conecte

bull Escribir y recibir datos del cliente por medio de las funciones write() y read() que son exactamente las mismas que usamos para escribir o leer de un fichero Obviamente tanto cliente como servidor deben saber queacute datos esperan recibir queacute datos deben enviar y en queacute formato Puedes ver coacutemo se pueden poner de acuerdo en estos mensajes en el apartado de mensajes

bull Cierre de la comunicacioacuten y del socket por medio de la funcioacuten close() que es la misma que sirve para cerrar un fichero

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes

bull Apertura de un socket como el servidor por medio de la funcioacuten socket()

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 6: Programacion de Socket en Linux

bull Solicitar conexioacuten con el servidor por medio de la funcioacuten connect() Dicha funcioacuten quedaraacute bloqueada hasta que el servidor acepte nuestra conexioacuten o bien si no hay servidor en el sitio indicado saldraacute dando un error En esta llamada se debe facilitar la direccioacuten IP del servidor y el nuacutemero de servicio que se desea

bull Escribir y recibir datos del servidor por medio de las funciones write() y read() bull Cerrar la comunicacioacuten por medio de close()

FICHEROS UNIXLINUX IMPLICADOS

Para la programacioacuten de socket no es estrictamente necesario ninguacuten fichero Sabiendo la direccioacuten IP y el nuacutemero de servicio se ponen directamente en coacutedigo y todo resuelto Sin embargo esto no es lo maacutes coacutemodo ni lo maacutes portable de unos ordenadores a otros

Hay dos ficheros en UnixLinux que nos facilitan esta tarea aunque hay que tener permisos de root para modificarlos Estos ficheros seriacutean el equivalente a una agenda de teleacutefonos en uno tenemos apuntados el nombre de la empresa con su nuacutemero de teleacutefono y en el otro fichero el nombre de la persona y su extensioacuten (EmpresaGorda tlfn 123456789 JoseGordo extensioacuten 2245 )

etchosts Esta es la agenda en la que tenemos las empresas y sus nuacutemeros de teleacutefono En este fichero hay una lista de nombres de ordenadores conectados en red y direccioacuten IP de cada uno Habitualmente en el etchosts del cliente se suele colocar el nombre del servidor y su direccioacuten IP Luego desde programa se hace una llamada a la funcioacuten gethostbyname() a la que pasaacutendole el nombre del ordenador como una cadena de caracteres devuelve una estructura de datos entre los que estaacute la direccioacuten IP

Una liacutenea de lo que puede aparecer en este fichero es la siguiente en el que vemos la direccioacuten IP y el nombre del ordenador que nos da el servicio de Quake

19230101 Ordenador_Quake

etcservices Este fichero es el equivalente a la agenda donde tenemos apuntados los distintos departamentospersonas de la empresa y sus nuacutemeros de extensioacuten telefoacutenica En este fichero hay una lista de servicios disponibles indicando nombre de servicio nuacutemero de servicio y tipo (ftpudp)

Tanto el servidor como el cliente deben tener en este fichero el servicio que estaacuten atendiendo solicitando con el mismo nuacutemero y tipo de servicio El nombre puede ser distinto igual que cada uno en su agenda pone el nombre que quiere pero no es lo habitual

Desde programa tanto cliente como servidor deben hacer una llamada a la funcioacuten getservbyname() a la que pasaacutendole el nombre del servicio devuelve una estructura de datos entre los que estaacute el nuacutemero de servicio y el tipo

Un ejemplo de lo que puede aparecer en un fichero etcservices es el siguiente en el que vemos en cada liacutenea nombre del servicio nuacutemero tipo y un comentario opcional

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 7: Programacion de Socket en Linux

detraacutes del siacutembolo Casualmente se ve el servicio www cuyo nuacutemero de servicio conocido por todos los ordenadores de Internet es el 80

tftp 69udp gopher 70tcp Internet Gopher gopher 70udp rje 77tcp finger 79tcp www 80tcp http Worldwide Web HTTP www 80udp HyperText Transfer Protocol link 87tcp ttylink

EJEMPLO

Sentadas las bases de los sockets vamos a ver un pequentildeo ejemplozip de programa servidor y cliente realizado con C en Linux El servidor esperaraacute la conexioacuten del cliente Una vez conectados se enviaraacuten una cadena de texto el uno al otro y ambos escribiraacuten en pantalla la cadena recibida

Para ejecutar el ejemplo no es necesario tener dos ordenadores Se pude ejecutar el servidor desde una ventana y el cliente desde otra En la ventana del servidor veremos la cadena que ha enviado el cliente y al reveacutes

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que indicamos anteriormente Explicaremos con cierto detalle queacute paraacutemetros se deben pasar y cual es el resultado de dichas llamadas

En primer lugar el servidor debe obtener el nuacutemero del servicio al que quiere atender haciendo la llamada a getservbyname() Esta funcioacuten devuelve una estructura (en realidad puntero a la estructura) en el que uno de sus campos contiene el nuacutemero de servicio solicitado

struct servent Puerto Estructura devuelta

La llamada a la funcioacuten Puerto = getservbyname (Nombre_Servicio tcp)

Los paraacutemetros de la funcioacuten son dos cadenas de caracteres La primera es el nombre del servicio tal cual lo escribimos en el fichero etcservices El segundo es el tipo de protocolo que queremos usar tcp o udp El nuacutemero de servicio que nos interesa estaacute en un campo de Puerto

Puerto-gts_port

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 8: Programacion de Socket en Linux

Ya tenemos todos los datos que necesita el servidor para abrir el socket asiacute que procedemos a hacerlo El socket se abre mediante la llamada a la funcioacuten socket() y devuelve un entero que es el descriptor de fichero o ndash1 si ha habido alguacuten error

int Descriptor Descriptor = socket (AF_INET SOCK_STREAM 0) if (Descriptor == -1) printf (Errorn)

El primer paraacutemetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros ordenadores distintos del servidor o van a correr en el mismo ordenador AF_INET vale para los dos casos AF_UNIX soacutelo para el caso de que el cliente corra en el mismo ordenador que el servidor pero lo implementa de forma maacutes eficiente Si ponemos AF_UNIX el resto de las funciones variacutea ligeramente

El segundo paraacutemetro indica si el socket es orientado a conexioacuten (SOCK_STREAM) o no lo es (SOCK_DGRAM) De esta forma podremos hacer sockets de red o de Unix de ambos tipos

El tercer paraacutemetro indica el protocolo a emplear Habitualmente se pone 0

Si se ha obtenido un descriptor correcto se puede indicar al sistema que ya lo tenemos abierto y que vamos a atender a ese servicio Para ello utilizamos la funcioacuten bind() El problema de la funcioacuten bind() es que lleva un paraacutemetro bastante complejo que debemos rellenar

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_port = Puerto-gts_port Direccionsin_addrs_addr =INADDR_ANY

if (bind (Descriptor (struct sockaddr )ampDireccion sizeof (Direccion)) == -1) printf (Errorn)

El paraacutemetro que necesita es una estructura sockaddr Lleva varios campos entre los que es obligatorio rellenar los indicados en el coacutedigo

sin_family es el tipo de conexioacuten (por red o interna) igual que el primer paraacutemetro de socket() sin_port es el nuacutemero correspondiente al servicio que obtuvimos con getservbyname() El valor estaacute en el campo s_port de Puerto Finalmente sin_addrs_addr es la direccioacuten del cliente al que queremos atender Colocando en ese campo el valor INADDR_ANY atenderemos a cualquier cliente

La llamada a bind() lleva tres paraacutemetros

bull Descriptor del socket que hemos abierto

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 9: Programacion de Socket en Linux

bull Puntero a la estructura Direccion con los datos indicados anteriormente La estructura admitida por este paraacutemetro es general y valida para cualquier tipo de socket y es del tipo struct sockaddr Cada socket concreto lleva su propia estructura Los AF_INET como este caso llevan struct sockaddr_in los AF_UNIX llevan la estructura struct sockaddr_un Por eso a la hora de pasar el paraacutemetro debemos hacer un cast al tipo struct sockaddr La operacioacuten cast es un pequentildeo truco que admite C Cuando tenemos un dato de un determinado tipo por ejemplo un entero int es posible convertirlo a otro tipo que nos venga mejor poniendo el nuevo tipo deseado entre pareacutentesis y detraacutes la variable del tipo no deseado Por ejemplo para convertir de entero a flotante

Variable_Flotante = (float)Variable_EnteraEsto es vaacutelido siempre y cuando los dos tipos tengan alguacuten tipo de relacioacuten y sea posible convertir uno en el otro En nuestro ejemplo las estructuras sockaddr_in y sockaddr_un pueden convertirse sin problemas al tipo sockaddr

bull Longitud de la estructura Direccion

La funcioacuten devuelve -1 en caso de error

Una vez hecho esto podemos decir al sistema que empiece a atender las llamadas de los clientes por medio de la funcioacuten listen()

if (listen (Descriptor 1) == -1) printf (Errorn)

La funcioacuten listen() admite dos paraacutemetros

bull Descriptor del socket bull Nuacutemero maacuteximo de clientes encolados Supongamos que recibimos la conexioacuten

de un primer cliente y le atendemos Mientras lo estamos haciendo se conecta un segundo cliente al que no atendemos puesto que estamos ejecutando el coacutedigo necesario para atender al primero Mientras sucede todo esto llega un tercer cliente que tambieacuten se conecta Estamos atendiendo al primero y tenemos dos en la lista de espera El segundo paraacutemetro de listen() indica cuaacutentos clientes maacuteximo podemos tener en la lista de espera Cuando un cliente entra en la lista de espera su llamada a connect() queda bloqueada hasta que se le atiende Si la lista de espera estaacute llena el nuevo cliente que llama a connect() recibiraacute un error de dicha funcioacuten

La funcioacuten listen() devuelve ndash1 en caso de error

Con todo esto ya soacutelo queda recoger los clientes de la lista de espera por medio de la funcioacuten accept() Si no hay ninguacuten cliente la llamada quedaraacute bloqueada hasta que lo haya Esta funcioacuten devuelve un descriptor de fichero que es el que se tiene que usar para hablar con el cliente El descriptor anterior corresponde al servicio y soacutelo sirve para encolar a los clientes Digamos que el primer descriptor es el aparato de teleacutefono de la telefonista de la empresa y el segundo descriptor es el aparato de teleacutefono del que estaacute atendiendo al cliente

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 10: Programacion de Socket en Linux

struct sockaddr Cliente int Descriptor_Cliente int Longitud_Cliente

Descriptor_Cliente = accept (Descriptor ampCliente ampLongitud_Cliente) if (Descriptor_Cliente == -1) printf (Errorn)

La funcioacuten accept() es otra que lleva un paraacutemetro complejo pero que no debemos rellenar Los paraacutemetros que admite son

bull Descriptor del socket abierto bull Puntero a estructura sockaddr A la vuelta de la funcioacuten esta estructura

contendraacute la direccioacuten y demaacutes datos del ordenador cliente que se ha conectado a nosotros

bull Puntero a un entero en el que se nos devolveraacute la longitud uacutetil del campo anterior

La funcioacuten devuelve un ndash1 en caso de error

Si todo ha sido correcto ya podemos hablar con el cliente Para ello se utilizan las funciones read() y write() de forma similar a como se hariacutea con un fichero Supongamos que sabemos que al conectarse un cliente nos va a mandar una cadena de cinco caracteres Para leerla seriacutea

int Leido = 0 Nuacutemero de caracteres leiacutedos hasta el momento int Aux = 0 Guardaremos el valor devuelto por read() int Longitud = 5 Nuacutemero de caracteres a leer char Datos[5] Buffer donde guardaremos los caracteres

Bucle hasta que hayamos leiacutedo todos los caracteres que estamos esperando while (Leido lt Longitud) Se leen los caracteres Aux = read (Descriptor Datos + Leido Longitud - Leido)

Si hemos conseguido leer alguacuten caraacutecter if (Aux gt 0) Se incrementa el nuacutemero de caracteres leiacutedos Leido = Leido + Aux else Si no se ha leiacutedo ninguacuten caraacutecter se comprueba la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1)

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 11: Programacion de Socket en Linux

printf (Errorn)

Parece un poco complicado A ver si somos capaces de aclararlo La funcioacuten read() admite como paraacutemetros

bull Descriptor del fichero socket del que se quiere leer bull Puntero a char donde se almacenaraacuten los datos leiacutedos bull Nuacutemero de caracteres que se quieren leer

La funcioacuten devuelve ndash1 si ha habido error 0 si el socket se ha cerrado o el fichero ha llegado a fin de fichero o bien el nuacutemero de caracteres si se ha conseguido leer alguno En el caso de socket si no hay errores y el socket sigue abierto y no hay caracteres para leer (no los han enviado desde el otro extremo) la llamada queda bloqueada

En el caso de leer de socket tenemos un pequentildeo problema antildeadido y es que se leen los caracteres disponibles y se vuelve Si pedimos 5 caracteres entra dentro de lo posible que read() lea 3 caracteres y vuelva sin dar error pero sin leer todos los que hemos pedido Por ese motivo al leer de socket es casi obligatorio (totalmente obligatorio si queremos transmitir muchos caracteres de un solo golpe) el leer con un bucle comprobando cada vez cuantos caracteres se han leiacutedo y cuantos faltan

Por este motivo read() va dentro de un while() en el que se mira si el total de caracteres leiacutedos es menor que el nuacutemero de caracteres que queremos leer La lectura del read() debe ademaacutes pasar como paraacutemetro cada vez la posicioacuten dentro del buffer de lectura en la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer (Longitud - Leido ) que tambieacuten cambia seguacuten se va leyendo

En cuanto a escribir caracteres la funcioacuten write() admite los mismos paraacutemetros con la excepcioacuten de que el buffer de datos debe estar previamente relleno con la informacioacuten que queremos enviar Tiene el mismo problema que escribe lo que puede y vuelve pudiendo no haber escrito toda la informacioacuten por lo que hay que hacer un trozo de coacutedigo similar

int Escrito = 0 Nuacutem de caracteres que ya se han escrito int Aux = 0 Nuacutemero de caracteres escritos en cada pasada int Longitud = 5 Total de caracteres a escribir char Datos[] = Hola El 5ordm caraacutecter es el 0 del final

Bucle mientras no se hayan escrito todos los caracteres deseados while (Escrito lt Longitud) Se escriben los caracteres Aux = write (Descriptor Datos + Escrito Longitud - Escrito)

Si hemos conseguido escribir alguacuten caraacutecter if (Aux gt 0)

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 12: Programacion de Socket en Linux

Incrementamos el nuacutemero de caracteres escritos Escrito = Escrito + Aux else Si no hemos podido escribir caracteres comprobamos la condicioacuten de socket cerrado if (Aux == 0) printf (Socket cerradon) else y la condicioacuten de error if (Aux == -1) printf (Errorn)

En un programa maacutes serio ni el cliente ni el servidor saben a priori queacute es lo que tienen que leer Normalmente hay una serie de mensajes que los dos conocen precedidos de una cabecera en la que suelen ir campos del estilo Identificador de mensaje y Longitud del mensaje De esta forma primero se leen estos dos campos y sabiendo queacute mensaje va a llegar y su longitud se procede a leerlo A la hora de escribir primero se manda la cabecera diciendo queacute mensaje se va a mandar y queacute longitud tiene y luego se manda el mensaje en siacute

Una vez que se han leiacutedo enviado todos los datos necesarios se procede a cerrar el socket Para ello se llama a la funcioacuten close() que admite como paraacutemetro el descriptor del socket que se quiere cerrar

close (Descriptor_Cliente)

Normalmente el servidor cierra el descriptor del cliente (Descriptor_Cliente) no el del socket (Descriptor) ya que este se suele dejar abierto para volver a realizar una llamada a accept() y sacar al siguiente cliente de la cola Es como si una vez que un amable sentildeor de una empresa nos ha atendido le dice a la telefonista que no atienda maacutes el teleacutefono Esas cosas soacutelo las hacen los jefes

DETALLES DEL CLIENTE

Puesto que la escritura lectura y cierre de sockets es ideacutentica a la del servidor uacutenicamente contaremos la apertura del socket que coacutemo no es maacutes compleja que la del servidor puesto que ademaacutes del nuacutemero de servicio debe obtener la direccioacuten IP del servidor

En primer lugar como en el servidor obtenemos el nuacutemero del servicio

struct servent Puerto Puerto = getservbyname (Nombre_Servicio tcp)

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 13: Programacion de Socket en Linux

if (Puerto == NULL) printf (Errorn)

Despueacutes obtenemos la direccioacuten IP del servidor El paraacutemetro que hay que pasar es una cadena de caracteres con el nombre del servidor tal cual lo pusimos en el fichero etchosts Devuelve los datos del servidor en una estructura hostent

struct hostent Host

Host = gethostbyname (Nombre_Servidor) if (Host == NULL) printf (Errorn)

Una vez que tenemos todos los datos necesarios abrimos el socket igual que en el servidor

Descriptor = socket (AF_INET SOCK_STREAM 0)

if (Descriptor == -1) printf (Errorn)

Ya tenemos todo lo necesario para solicitar conexioacuten con el servidor El paraacutemetro de connect() ya es conocido y soacutelo tenemos que rellenarlo igual que en bind() del servidor

struct sockaddr_in Direccion Direccionsin_family = AF_INET Direccionsin_addrs_addr = ((structin_addr)(Host-gth_addr))-gts_addr Direccionsin_port = Puerto-gts_port

if (connect (Descriptor (struct sockaddr )ampDireccionsizeof (Direccion)) == -1) printf (Errorn)

La uacutenica novedad es que la direccioacuten del servidor se coloca en Direccionsin_addrs_addr y no como pusimos antes INADDR_ANY Dicha direccioacuten la tenemos dentro de la estructura obtenida con gethostbyname() en el campo ((structin_addr)(Host-gth_addr))-gts_addr iexclVaya por Dios iexclMenudo campo Nos pasa como dijimos antes los tipos no son exactamente los mismos y tenemos estructuras anidadas en estructuras con lo que la sintaxis queda de esa forma tan horrible La explicacioacuten es la siguiente

La estructura Host tiene un campo h_addr de ahiacute la parte Host-gth_addr

Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr)(Host-gth_addr)

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 14: Programacion de Socket en Linux

Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((structin_addr)(Host-gth_addr))-gts_addr

El resto es exactamente igual que en el servidor

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets

El ejemplo aquiacute expuesto funciona correctamente pero si se quieren hacer otras cosas un poco maacutes serias hay que tener en cuenta varios puntos que indicamos aquiacute por encima

En primer lugar el ejemplo es con sockets orientados a conexioacuten y de tipo AF_INET ( para que pueda funcionar con servidor en un ordenador y cliente en otro) Si cambiamos de tipo de socket o de conexioacuten la idea baacutesica es la misma pero la sintaxis del coacutedigo cambia ligeramente Antes de realizar dicho cambio conviene saber queacute cosas hay que cambiar en el coacutedigo Un ejemplo claro si usamos sockets no orientados a conexioacuten sobra la llamada a connect() que hace el cliente y la de accept() que hace el servidor pero en su lugar el cliente debe hacer una llamada a bind() Puedes ver aquiacute un ejemplo de socket udp

Si usamos sockets AF_UNIX sobra pedir la direccioacuten IP del servidor ya que es el mismo ordenador en el que corren ambos programas

Organizacioacuten de los enteros

En el mundo de los ordenadores estaacuten los micros de intel (y los que los emulan) y los demaacutes La diferencia entre otras muchas es que organizan los enteros de forma distinta uno pone antes el byte maacutes significativo del entero y el otro lo pone al final Si conectamos dos ordenadores con sockets una de las informaciones que se transmiten en la conexioacuten es un entero con el nuacutemero de servicio iexclYa la hemos liado hay micros que no se entienden entre siacute Para evitar este tipo de problemas estaacuten la funciones htons() htonl() y similares Esta funcioacuten convierte los enteros a un formato standard de red con lo que se garantiza que nos podemos entender con cualquier entero Eso implica que algunos de los campos de las estructuras que hemos rellenado arriba debemos aplicarles esta funcioacuten antes de realizar la conexioacuten Si enviamos despueacutes enteros con write() o los leemos con read() debemos convertirlos y desconvertirlos a formato red

Puedes ver un ejemplo de todo esto al conectar un socket en Java con uno en C Aunque ambos corran en el mismo ordenador java tiene su propia maacutequina virtual por lo que es como si corriera en su propio microprocesados distinto de los pentium

Atencioacuten al cliente

Una teacutecnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada cliente Puedes echar un ojo a la funcioacuten fork()Si no se quieren crear procesos se puede

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 15: Programacion de Socket en Linux

usar la funcioacuten select() A esta funcioacuten se le dicen todos los sockets que estamos atendiendo y cuando la llamemos nos quedamos bloqueados hasta que en alguno de ellos haya actividad Esto nos evita estar en un bucle mirando todos los sockets clientes uno por uno para ver si alguno quiere algo

Se suelen crear procesos con fork() cuando el servidor no es capaz de atender todas las peticiones de los clientes a suficiente velocidad Al tener un proceso dedicado a cada cliente se puede atender a varios simultaneamente Se suele usar select() cuando podemos atender a los clientes lo suficientemente raacutepido como para hacerlo de uno en uno sin hacer esperar demasiado a nadie

Poniendo un ejemplo si en una ventanilla de un banco gotean los clientes y se despacha raacutepido a cada uno basta con una uacutenica ventanilla y una persona en ella que duerma disimuladamente mientras llega un cliente Si los clientes llegan a mogolloacuten y se tarda en atender a cada uno de ellos es mejor que haya muchas ventanillas lo ideal una por cada cliente

BIBLIOGRAFIacuteA

Un excelente libro para programacioacuten avanzada en C sobre entorno Unix es UNIX Programacioacuten avanzada de Fco Manuel Maacuterquez Garciacutea editorial ra-ma

Tienes una guia bastante simple pero maacutes detallada de sockets en httpwwwarrakises~dmrqbeejindexhtml

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 16: Programacion de Socket en Linux

Inet (char ) Abre un socket AF_INET Este tipo de socket siacute sale por red y permite comunicar programas que se ejecuten en distintos ordenadores Se le pasa un nombre de servicio tcp que esteacute dado de alta en el fichero etcservices Devuelve el descriptor del socket servidor y debemos guardaacuternoslo -1 en caso de error

int Acepta_Conexion_Cliente (int) Se queda bloqueada hasta que un cliente se conecte Se le pasa el descriptor de socket servidor obtenido con cualquiera de las funciones anteriores Devuelve el descriptor del socket cliente con el que podremos leer o escribir datos al cliente -1 en caso de error

int Abre_Socket_Udp (char )

Abre un socket UDP para atender el servicio cuyo nombre se le pasa como paraacutemetro El servicio debe estar dado de alta en etcservicies como udp La funcioacuten devuelve el descriptor del socket si todo ha ido bien o -1 en caso de error

CLIENTE

int Abre_Conexion_Unix (char ) Establede una conexioacuten AF_UNIX con el servidor Se le pasa el mismo fichero que se pasoacute en el servidor a la funcioacuten Abre_Socket_Unix() con el mismo path Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Inet (char ) Establece una conexioacuten AF_INET con el servidor Se le pasa el mismo nombre de servicio que se le pasoacute al servidor en la funcioacuten Abre_Socket_Inet() y que estaacute dado de alta en el fichero etcservices Devuelve el descriptor de fichero que nos permite enviar o recibir datos del servidor -1 en caso de error

int Abre_Conexion_Udp () Abre un socket UDP para un cliente El sistema

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 17: Programacion de Socket en Linux

operativo elige un serviciopuerto que esteacute libre La funcioacuten nos devuelve el descriptor del socket o -1 si ha habido alguacuten error

COMUNES

int Lee_Socket (int char int) Sirve para leer datos de un socket abierto Se le pasa

bull el descriptor obtenido con la funcioacuten Abre_Conexion_Unix() Acepta_Conexion_Inet() o Acepta_Conexion_Cliente()

bull un buffer en el que queramos recibir (puede ser un puntero a cualquier estructura de datos que ya esteacute creada haciendo el cast adecuado)

bull el nuacutemero de bytes que queremos leer del socket (y que deben caber en el buffer que pasemos)

A la vuelta el buffer queda relleno con los datos leidos del socket Devuelve el nuacutemero de bytes leido si no ha habido problemas 0 si en el otro extremo han cerrado el socket y -1 en caso de error

int Escribe_Socket (int char int) Como la anterior pero para escribir datos El buffer debe contener los datos que queremos enviar

int Lee_Socket_Udp (int struct sockaddr int char int)

Lee un mensaje de un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() La funcioacuten se queda bloqueada hasta que llegue el mensaje o se produzca alguacuten error Se le pasa

bull El descriptor del socket del que debe leer

bull Una estructura sockaddr

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 18: Programacion de Socket en Linux

Cuando se lea el mensaje en esta estructura la funcioacuten nos devolveraacute los datos del que nos ha enviado el mensaje Debemos guardarlos si queremos responderle

bull Un puntero a entero El entero debe contenter el tamantildeo de la estructura anterior

bull El buffer donde la funcioacuten dejaraacute el mensaje Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull La longitud del mensaje que queremos leer

La funcioacuten devuelve el nuacutemero de bytes leidos o -1 en caso de error

int Escribe_Socket_Udp (int struct sockaddr int char int)

Enviacutea un mensaje por un socket UDP obtenido con Abre_Conexion_Udp() o Abre_Socket_Udp() Se le pasa

bull El descriptor del socket por el que va a enviar el mensaje

bull Una estructura sockaddr con el destinatario del mensaje Esta estructura la obtenemos con la funcioacuten anterior al recibir un mensaje o bien con la funcioacuten Dame_Direccion_Udp()

bull Un entero con el tamantildeo de la estructura anterior

bull El buffer con el mensaje que queremos enviar Puede ser un puntero a cualquier estructura de datos que queramos haciendo el cast a char

bull Un entero con el tamantildeo del mensaje que queremos enviar (que estaacute contenido en el buffer anterior)

La funcioacuten devuelve el nuacutemero de bytes escritos en el socket o -1 en caso de error

int Dame_Direccion_Udp (char Esta funcioacuten es uacutetil para rellenar

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 19: Programacion de Socket en Linux

char struct sockaddr int )

estructuras sockaddr de una forma sencila Se le pasa

bull el nombre de un host (dado de alta en etchosts) Puede ser NULL si la estructura se usa para un bind() de un servidor udp

bull el nombre de un servicio (dado de alta en etcservices como udp) Puede ser NULL si queremos que el sistema operativo elija un serviciopuerto que esteacute libre (uacutetil para bind() de clientes udp)

bull un puntero a una estructura sockaddr que nos devolveraacute rellena con el host y servicio indicado

bull un puntero a entero en el que pasamos el tamantildeo de la estructura Este paraacutemetro se ignora

La funcioacuten devuelve -1 en caso de error Ojo He visto un error falta un return 0 al final con lo que puede fallar aunque lo haga bien

Para cerrar los sockets basta con llamar a la funcioacuten close() pasaacutendole el descriptor del socket No he hecho una funcioacuten para eso

Para obtener la libreriacutea necesitas los ficheros Socketc Socketh Socket_Servidorc Socket_Servidorh Socket_Clientec Socket_Clienteh y Makefile Descaacutergalos quiacutetales la extensioacuten txt y compilalos con make Se crearaacute un libreriacutea libChSocketa Acueacuterdate del directorio donde dejas todo esto que en los ejemplos te haraacute falta indicar el path de esta libreriacutea y de sus h Tambieacuten puedes descargarlo todo junto con ChSockettargz

Para utilizarla con tus propios programas si ltpathgt es el directorio donde has puesto todo esto la orden de compilacioacuten seriacutea parecida a esto

cc miprogramac -Iltpathgt -Lltpathgt -lChSocket -o miprograma

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 20: Programacion de Socket en Linux

ejemplo de coacutedigo con esta segunda opcioacuten Un programa servidor atenderaacute conexiones de clientes A cada uno le asignaraacute un nuacutemero de cliente y se lo enviaraacute Tendremos un programa cliente que cada segundo enviacutea su nuacutemero de cliente al servidor Podremos lanzar hasta 10 clientes simultaneamente y todos seraacuten atendidos En el ejemplo baacutesico de sockets se hicieron unos ficheros con funciones uacutetiles Aquiacute se han extraido en una libreriacutea que se necesita para el ejemplo

Para facilitarnos el tratamiento con un solo proceso tenemos la funcioacuten select() Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores podemos pasarselos a la funcioacuten select() Si asiacute lo deseamos nuestro coacutedigo se quedaraacute dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos enviacutea un mensaje)

Los paraacutemetros de la funcioacuten select() son los siguientes

bull int con el valor del descriptor maacutes alto que queremos tratar maacutes uno Cada vez que abrimos un fichero socket o similar se nos da un descriptor de fichero que es entero Estos descriptores suelen tener valores consecutivos El 0 suele estar reservado para la stdin el 1 para la stdout el 2 para la stderr y a partir del 3 se nos iraacuten asignando cada vez que abramos alguacuten fichero Aquiacute debemos dar el valor maacutes alto del descriptor que queramos pasar a la funcioacuten maacutes uno

bull fd_set es un puntero a los descriptores de los que nos interesa saber si hay alguacuten dato disponible para leer o que queremos que se nos avise cuando lo haya Tambieacuten se nos avisaraacute cuando haya un nuevo cliente o cuando un cliente cierre la conexioacuten

bull fd_set es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro Si en el otro lado han cerrado la conexioacuten e intentamos escribir se nos enviaraacute una sentildeal SIGPIPE que haraacute que nuestro programa se caiga (salvo que tratemos la sentildeal) Para nuestro ejemplo no nos interesa

bull fd_set es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepcioacuten Para nuestro ejemplo no nos interesa

bull struct timeval es el tiempo que queremos esperar como maacuteximo Si pasamos NULL nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores Se puede poner un tiempo cero si uacutenicamente queremos saber si hay algo en alguacuten descriptor sin quedarnos bloqueados

Cuando la funcioacuten retorna nos cambia los contenidos de los fd_set para indicarnos queacute descriptores de fichero tiene algo Por ello es importante inicializarlos completamente antes de volver a llamar a la funcioacuten select()

Estos fd_set son unos punteros un poco raros Para rellenarlos y ver su contenido tenemos una serie de macros

bull FD_ZERO (fd_set ) nos vaciacutea el puntero de forma que estamos indicando que no nos interesa ninguacuten descriptor de fichero

bull FD_SET (int fd_set ) mete el descriptor que le pasamos en int al puntero fd_set De esta forma estamos indicando que tenemos interes en ese descriptor

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 21: Programacion de Socket en Linux

Llamando primero a FD_ZERO() para inicializar el contenido del puntero y luego a FD_SET() tantas veces como descriptores tengamos ya tenemos la variable dipuesta para llamar a select()

bull FD_ISSET (int fd_set ) nos indica si ha habido algo en el descriptor int dentro de fd_set Cuando select() sale debemos ir interrogando a todos los descriptores uno por uno con esta macro

bull FD_CLEAR (int fd_set ) elimina el descriptor dentro del fd_set

En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes Inicializaremos fd_set con un FD_ZERO() luego le antildeadiremos el socket servidor y finalmente con un bucle los sockets clientes Despueacutes llamaremos a la funcioacuten select() El coacutedigo seriacutea maacutes o menos

fd_set descriptoresLectura int socketServidor int socketCliente[10] int numeroClientes FD_ZERO (ampdescriptoresLectura) FD_SET (socketServidor ampdescriptoresLectura) for (i=0 iltnumeroClientes i++) FD_SET (socketCliente[i] ampdescriptoresLectura) select (maximo+1 ampdescriptoresLectura NULL NULL NULL)

Como no tenemos interes en condiciones de escritura ni excepciones pasamos NULL en el segundo y tercer paraacutemetro El uacuteltimo lo ponemos tambieacuten a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos enviacutee algo

Cuando se salga del select() es porque 1) se ha intentado conectar un nuevo cliente 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexioacuten En cualquiera de estas circunstancias tenemos que hacer el tratamiento adecuado La funcioacuten select() soacutelo nos avisa de que algo ha pasado pero no acepta automaacuteticamente al nuevo cliente no lee su mensaje ni cierra su socket

Por ello detraacutes del select() debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[] para ver si nos han enviado algo o cerrado el socket El coacutedigo despueacutes del select() seriacutea

Se tratan los clientes for (i=0 iltnumeroClientes i++) if (FD_ISSET (socketCliente[i] ampdescriptoresLectura)) if ((Lee_Socket (socketCliente[i] (char )ampbuffer sizeof(int)) gt 0)) Se ha leido un dato del cliente correctamente Hacer aquiacute el tratamiento para ese mensaje En el ejemplo se lee y se escribe en pantalla

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 22: Programacion de Socket en Linux

else Hay un error en la lectura Posiblemente el cliente ha cerrado la conexioacuten Hacer aquiacute el tratamiento En el ejemplo se cierra el socket y se elimina del array de socketCliente[]

Se trata el socket servidor if (FD_ISSET (socketServidor ampdescriptoresLectura)) Un nuevo cliente solicita conexioacuten Aceptarla aquiacute En el ejemplo se acepta la conexioacuten se mete el descriptor en socketCliente[] y se enviacutea al cliente su posicioacuten en el array como nuacutemero de cliente

La funcioacuten Lee_Socket() forma parte de la libreriacutea que se comentoacute anteriormente Devuelve lo mismo que la funcioacuten read() es decir el nuacutemero de bytes leidos 0 si se ha cerrado el socket o -1 si ha habido error

En cuanto al coacutedigo de ejemplo del cliente poco tiene que decir Abre la conexioacuten recibe un nuacutemero de cliente del servidor y se lo reenvia una vez por segundo

En primer lugar pera ejecutar el ejemplo necesitas una mini libreriacutea de socket que he hecho para no tener que repetir el mismo coacutedigo en todos los ejemplos

Una vez que tengas la libreriacutea tienes los coacutedigos de ejemplo en servselectc y clientselectc que se compilan con Makefile Descaacutergalos en un directorio distinto al de la libreriacutea (ya que el fichero Makefile aunque con el mismo nombre es distinto del de la libreriacutea) quita la extensioacuten txt Edita el Makefile del ejemplo y en la liacutenea que pone

LIBCHSOCKET = LIBRERIA

cambia LIBRERIA por el path donde hayas descargado y compilado la libreriacutea Compila el ejemplo con el comando make

Con permisos de root en el fichero etcservices debes antildeadir una liacutenea que ponga

cpp_java 15557tcp

El nuacutemero puede ser el que tuacute quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero El nombre cpp_java aparece tal cual en el coacutedigo del ejemplo Si quieres puedes poner otro nombre en el etcservices pero debes cambiarlo tambieacuten en los fuentes del ejemplo

Una vez compilado todo ejecuta el servidor con servselect Luego puedes ejecutar en varias ventanas tantos clientes clientselect como desees Veraacutes como todos son atendidos hasta un maacuteximo de 10 simultaacuteneamente

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 23: Programacion de Socket en Linux

MENSAJES ENTRE SOCKETS

En los ejemplos hasta ahora (ejemplo simple ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro Esto para una aplicacioacuten real es demasiado simple Lo normal es que entre un cliente y un servidor se intercambien informacioacuten maacutes compleja estructuras de datos completas Vamos a ver cuales son los mecanismos habituales para esta transmisioacuten de mensajes entre sockets

LAS ESTRUCTURAS DE DATOS

Supongamos por ejemplo que el cliente puede enviar al servidor los siguientes mensajes

bull Pedir la fecha y hora No necesita enviar datos siacutemplemente un algo que haga que el servidor le devuelva la fecha hora

bull Pedir el diacutea de la semana El cliente enviacutea al servidor una fecha y espera que este le devuelva una cadena de caracteres con el diacutea de la semana

El servidor como respuesta podraacute enviar al cliente los mensajes

bull Un long que es el nuacutemero de segundos que han transcurrido desde el 1 de Enero de 1970 Este entero es la forma que tiene unix por medio de la funcioacuten time() de darnos la fechahora del sistema

bull Una cadena de caracteres (pongamos 10 caracteres para que quepa Mieacutercoles con su fin de cadena 0 al final) En el coacutedigo vamos a poner 12 caracteres y maacutes adelante explicareacute por queacute

Lo primero que hay que hacer es escribir en un fichero h todas estas estructurasmensajes Podemos si queremos ser maacutes ordenados o si hay muchos mensajes escribir dos ficheros h En uno iraacuten las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente Puesto que en este caso es muy sencillo soacutelo hay tres estructuras de datos lo haremos todo junto

No hay estructura de datos para que el cliente pida al servidor la fechahora

Estructura de datos para que el cliente pida al servidor el diacutea de la semana typedef struct MensajeDameFecha long fechaHora MensajePedirFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide la fechahora typedef estruct MensajeTomaFecha long fechaHora MensajeTomaFecha

Estructura de datos que devuelve el servidor al cliente cuando se le pide el diacutea de la semana

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 24: Programacion de Socket en Linux

typedef struct MensajeTomaDiaSemana char diaSemana[12] MensajeTomaDiaSemana

Un par de pequentildeos detalles

bull Cuando se enviacutea un mensaje por un socket se enviacutea los bytes que componen esa estructura Por ello hacer una estructura que contengan punteros es un error Cuando enviemos dicha estructura con un puntero enviaremos por el socket la direccioacuten de memoria a la que apunta el puntero (no los datos) Esa direccioacuten de memoria no tiene ninguacuten sentido en el proceso que recibe el mensaje (en linux cada proceso tiene su propio espacio de memoria virtual y maacutes si estaacute en otro ordenador distinto) Si podemos sin embargo poner arrays como diaSemana siempre y cuando ya tengan su tamantildeo prefijado y fijo

bull Los float y double no son buena eleccioacuten para poner en una estructura que va a hacer de mensaje Si el mensaje va entre dos ordenadores iguales no hay problema pero entre dos ordenadores distintos es posible que codifiquen internamente los bytes de distinta manera Dichos campos flotantes no se interpretaraacuten correctamente Pueden usarse siempre que seamos conscientes de este problema

bull Veamos el por queacute de los 12 caracteres en el diacutea de la semana en vez de 10 Nuestros ordenadores son de 32 bits (casi todos) Por ello 32 bits (4 bytes) se convierte en una especie de nuacutemero maacutegico Cuando creamos una estructura nuestro compilador se encarga de sus campos queden maacutes o menos alineados con muacuteltiplos de 4 bytes Por ejemplo si ponemos

typedef struct UnaEstructura char caracter int numero UnaEstructura

printf (El tamantildeo es dn sizeof(UnaEstructura))

asombrosamente nos saldraacute 8 El campo caracter ocupa 1 byte numero ocupa 4 bytes Los otros 3 que faltan los ha metido el compilador entre caracter y numero para hacer que todo cuadre con muacuteltiplos de 4 bytes Cuando enviemos la estructura por el socket estaremos enviando los 8 bytes Por ello suele ser conveniente o bien ser consciente de esto o bien hacer que nuestras estructurascampos sean muacuteltiplos exactos de 4

LA CABECERA DEL MENSAJE

El primer problema que encontramos cuando hay varios mensajes es coacutemo identificar queacute mensaje vamos a recibir cuaacutentos bytes tenemos que leer Por ello antes

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 25: Programacion de Socket en Linux

de enviar un mensaje por un socket se suele enviar una estructura comuacuten que hace de cabecera del mensaje En esa cabecera va la informacioacuten necesaria para identificar el mensaje (que se suele llamar cuerpo) que se va a enviar despueacutes El que recibe el mensaje primero lee la cabecera y cuando sabe queacute mensaje es el que va detraacutes lo lee

iquestQueacute contiene una cabecera

El uacutenico campo imprescindible para la cabecera es un identificador del mensaje que va detraacutes Habitualmente suele ser un entero de forma que si su valor es 0 el mensaje que va detraacutes es uno determinado si es 1 es otro y asiacute sucesivamente

typedef struct Cabecera int identificador Cabecera

Es habitual tambieacuten hacer que este entero sea un enumerado Cada valor del enumerado es un identificador de un mensaje distinto Igual que antes se puede hacer un uacutenico enumerado para todos los mensajes o bien dos enumerados de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor y en el segundo los del servidor al cliente Hay que poner enumerado tambieacuten para los mensajes que no llevan datos

typedef Identificadores enum IdDameFecha IdDameDiaSemana IdTomaFecha IdTomaDiaSemana Identificadores

Para enviar el mensaje por ejemplo con la funcioacuten write() se hariacutea lo siguiente

MensajeDameDiaSemana mensaje Cabecera cabecera

mensaje = Se rellenan los datos del mensaje cabeceraidentificador = IdDameDiaSemana Se rellena la cabecera

Se enviacutea primero la cabecera y luego el mensaje write (socket ampcabecera sizeof(cabecera)) write (socket ampmensaje sizeof(mensaje))

La lectura es algo maacutes compleja Si leemos con la funcioacuten read() nos quedariacutea algo asiacute como

Cabecera cabecera

Leemos la cebecera read (scket ampcabecera sizeof(cabecera))

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 26: Programacion de Socket en Linux

En funcioacuten de la cabecera leida leemos el mensaje que va a continuacioacuten switch (cabeceraidentificador) case IdDameFecha No hay que leer mas tratamiento del mensaje break

case IdDameDiaSemana MensajeDameDiaSemana mensaje Se declara una variable del mensaje que queremos leer read (socket ampmensaje sizeof(mensaje)) Se lee tratamiento del mensaje break

Vemos que es necesario un switch o similar de forma que cada case lee y trata uno de los posibles mensajes

IDEAS PARA UNA PEQUENtildeA LIBRERIacuteA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas liacuteneas de coacutedigo que deberaacuten realizarse con frecuencia no es mala idea hacer una funcioacuten que nos facilite la tarea y que de paso nos oculte la estructura exacta de la cabecera Por ejemplo el prototipo de la funcioacuten podriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho)

de forma que socket es el socket por el que queremos enviar el mensaje idMensaje es el entero identificador del mensaje mensaje es un puntero a la estructura del mensaje y tamanho es el tamantildeo de dicha estructura-mensaje El coacutedigo de la funcioacuten en forma simple pordriacutea ser

void escribeMensaje (int socket int idMensaje char mensaje int tamanho) Se declara y rellena la cabecera Cabecera cabecera cabeceraidentificador = idMensaje

Se enviacutea la cabecera write (socket ampcabecera sizeof(cabecera)

Si el mensaje no tiene cuerpo hemos terminado if ((mensaje == NULL) || (tamanho == 0)) return

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 27: Programacion de Socket en Linux

Se enviacutea el cuerpo write (socket mensaje tamnho)

Una vez hecha la funcioacuten su uso es simple

Se declara y rellena el mensaje que queremos enviar MensajeDameDiaSemana mensaje mensaje =

Se enviacutea llamando a nuestra funcioacuten de libreriacutea escribeMensaje (socket IdDameDiaSemana (char )ampmensaje sizeof(mensaje))

Para la lectura podemos hacer algo similar El prototipo de la funcioacuten seriacutea

void leeMensaje (int socket int idMensaje char mensaje)

socket es el socket por el que queremos leer idMensaje es un puntero a entero La funcioacuten nos lo devolveraacute relleno con el identificador del mensaje que ha recibido mensaje es un puntero a un puntero a char (para ver por queacute mira en punteros como paraacutemetros) La funcioacuten crearaacute la memoria necesaria para leer el mensaje y nos la devolveraacute como puntero a char Nosotros fuera de la funcioacuten haremos el cast adecuado en funcioacuten del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos

Si nos ponemos a hacer el coacutedigo tendremos algo como esto

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera

Se lee la cabecera read (socket ampcabecera sizeof(cabecera))

switch (cabeceraidentificador) case creo que hay un problema

Pues parece que tenemos un problema Si empezamos a hacer los case nuestra funcioacuten va a depender de la mensajeriacutea especiacutefica de nuestra aplicacioacuten Deberemos rehacer esta funcioacuten cada vez que hagamos una aplicacioacuten con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo Esto no es muy adecuado para una libreriacutea de funciones que queramos que sea maacutes o menos general

Para resolver este problema tenemos necesidad de antildeadir un nuevo campo a la estructura Cabecera un campo que indique la longitud del mensaje

typedef struct Cabecera

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 28: Programacion de Socket en Linux

int identificador int longitud Longitud del mensaje en bytes Cabecera

Con esto nuestra funcioacuten de escribir no se ve afectada (uacutenicamente hay que rellenar el nuevo campo con el paraacutemetro tamanho antes de enviar la cabecera) Sin embargo se nos facilita enormemente la funcioacuten de leer Despueacutes de leer la cabecera hay que crear un buffer del tamantildeo que indique el campo longitud y leer en ese buffer la estructura sin necesidad de saber queacute estructura es

void leeMensaje (int socket int idMensaje char mensaje) Cabecera cabecera mensaje = NULL Ponemos el mensaje a NULL por defecto

read (socket ampcabecera sizeof(cabecera)) Se lee la cabecera

Rellenamos el identificador para devolverlo idMensaje = cabeceraidentificador

Si hay que leer una estructura detraacutes if (cabeceralongitud gt 0) mensaje = (char )malloc (longitud) Se reserva espacio para leer el mensaje

read (socket mensaje longtud)

Con la funcioacuten hecha asiacute su utilizacioacuten seriacutea parecida a esto

char mensaje = NULL int identificador

Se lee el mensaje leeMensaje (socket ampidentificador ampmensaje)

Se hace el tratamiento del mensaje seguacuten el identificador switch (identificador) case IdDameFecha tratamiento del IdDameFecha No hay mensaje asociado break

case IdDameDiaSemana Se hace un cast de char a MensajeDameDiaSemana para tener maacutes accesibles los datos recibidos

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 29: Programacion de Socket en Linux

MensajeDameDiaSemana mensajeDiaSemana = NULL mensajeDiaSemana = (MensajeDameDiaSemana )mensaje tratamiento de mensajeDiaSemana break

Se libera el mensaje cuando ya no lo necesitamos if (mensaje = NULL) free (mensaje) mensaje = NULL

Vemos que no nos libramos del switch-case pero en este caso queda en la parte de la aplicacioacuten y no en la funcioacuten de la libreriacutea general Por otra parte es lo loacutegico puesto que la aplicacioacuten deberaacute tratar de distinta manera los mensajes

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos maacutes que suficiente Sin embargo hay aplicaciones que antildeaden maacutes campos informativos a la cabecera No vamos a entrar en detalle pero algunos de ellos pueden ser

bull FechaHora de enviacuteo del mensaje Ademaacutes de ser informativo puede darnos posibilidad de detectar que las comunicaciones van demasiado lentas y darnos la posibilidad de tirar sin tratar los mensajes que se reciban mucho despueacutes que la fechahora contenida en la cabecera De esta forma quizaacutes se pueda recuperar el tiempo de retraso Nuestra funcioacuten de leer podriacutea faacutecilmente implementar este comportamiento

bull Checksum Un entero que es la suma de todos los bytes del mensaje Puede o no incluir la cabecera seguacuten decidamos implementarlo Se usa como comprobacioacuten de que el mensaje ha llegado correctamente Al recibir el mensaje el lector realiza la suma de todos los bytes recibidos y lo compara con este campo checksum Si coinciden todo es correcto si no coinciden se ha producido un error En un protocolo de sockets TCPIP o UDPIP este campo no tiene demasiado sentido ya que el protocolo nos garantiza que el mensaje llega correctamente e intermamente ya implementa sus propios mecanismos de comprobacioacuten Si antildeadimos este campo estaremos repitiendo algo que ya estaacute hecho y que SIEMPRE nos va a dar correcto Este campo tiene maacutes sentido para otras comunicaciones menos fiables como un puerto serie sin bit de paridad

bull Nuacutemero maacutegico Cuando leemos suponemos que nos llega una cabecera y un mensaje una cabecera y un mensaje y asiacute sucesivamente Ademaacutes la longitud del mensaje va contenida en la cabecera Si al leer o enviar nos equivocamos y se enviacutea algo incorrecto es totalemente imposible leer los siguientes mensajes ya que nada estaraacute donde esperamos empezaremos a leer bytes pensando que son una cabecera cuando en realidad no lo son etc A esta situacioacuten la llamaremos peacuterdida de sincronismo Para detectar esta situacioacuten y tratar de corregirla suele ser habitual hacer que la

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 30: Programacion de Socket en Linux

cabecera tenga un primer campo entero llamado nuacutemero maacutegico y que siempre contenga un mismo valor fijo Este valor debe ser un valor que no sea probable de encontrar en el mensaje por ejemplo 0xFF00FF00 Cuando al leer mensajes vemos que no nos cuadran las cosas (identificadores fuera de rango longitudes negativas o descabelladas) debemos empezar a leer en el socket los bytes de uno en uno hasta encontrar un nuacutemero maacutegico Tendremos entonces bastantes posibilidades de que eso sea el principio de una cabecera y podremos recuperar el sincronismo

CONCLUSIONES

Con esto queda explicado el coacutemo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra libreriacutea de sockets

Es maacutes estas dos funciones son muy generales puesto que no tienen nada que ver con sockets El paraacutemetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero vaacutelido (un socket un puerto serie o un fichero) Se podriacutean poner estas funciones en una libreriacutea separada y usarlas para otros tipos de comunicacioacuten o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera) Para leer dicho fichero se leeriacutea cabecera estructura cabecera estructura y asiacute sucesivamente

Conceptos

Seguacuten vamos haciendo programas de ordenador nos damos cuenta que algunas partes del coacutedigo se utilizan en muchos de ellos Por ejemplo podemos tener varios programas que utilizan nuacutemeros complejos y las funciones de suma resta etc son comunes Tambieacuten es posible por ejemplo que nos guste hacer juegos y nos damos cuenta que estamos repitiendo una y otra vez el coacutedigo para mover una imagen (un marcianito o a Lara Croft) por la pantalla

Seriacutea estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas de forma que podamos usarlas siempre que queramos Las ventajas enormes de esto son

bull No tener que volver a escribir el coacutedigo (o hacer copy-paste) bull Nos ahorraremos el tiempo de compilar cada vez ese coacutedigo que ya estaacute

compilado Ademaacutes ya sabemos que mientras hacemos un programa probamos y corregimos hay que compilar entre muchas y maacutes muchas veces

bull El coacutedigo ya compilado estaraacute probado y seraacute fiable No las primeras veces pero siacute cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores

La forma de hacer esto es hacer libreriacuteas Una libreriacutea son una o maacutes funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa Por ejemplo si hacemos

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 31: Programacion de Socket en Linux

nuestra funcioacuten de mover la imagen de Lara Croft tendremos que hacer la funcioacuten de forma que admita cualquier imagen ya que no nos pegariacutea nada Lara Croft dando saltos en un juego estilo space invaders

Coacutemo tenemos que organizar nuestro coacutedigo

Para poder poner nuestro coacutedigo en una libreriacutea necesitamos organizarlo de la siguiente manera

bull Uno o maacutes ficheros fuente c con el coacutedigo de nuestras funciones bull Uno o maacutes ficheros de cabecera h con los tipos (typedefs structs y enums) y

prototipos de las funciones que queramos que se puedan utilizar

Como siempre vamos a hacer un ejemplo Los ficheros seriacutean estos

libreria1h libreria1c

ifndef _LIBRERIA_1_H define _LIBRERIA_1_H

int suma (int a int b) int resta (int a int b)

endif

int suma (int a int b) return a+b

int resta (int a int b) return a-b

Es un fichero con un par de funciones simples de suma() y resta()

Un detalle importante a tener en cuenta son los define del fichero de cabecera (h) Al hacer una libreriacutea no sabemos en queacute futuros programas la vamos a utilizar ni coacutemo estaraacuten organizados Supongamos en un futuro programa que hay un fichero de cabecera fichero1h que hace include del nuestro Imaginemos que hay tambieacuten un fichero2h que tambieacuten hace include del nuestro Finalmente con un pequentildeo esfuerzo maacutes imaginemos que hay un tercer fichero3c que hace include de fichero1h y fichero2h es decir maacutes o menos lo siguiente

fichero1h fichero2h fichero3c

include ltlibreria1hgt

include ltlibreria1hgt

include ltfichero1hgt include ltfichero2hgt

Cuando compilemos fichero3c dependiendo de lo que haya definido en libreria1h obtendremos un error El problema es que al incluir fichero1h se define todo lo que

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 32: Programacion de Socket en Linux

haya en ese fichero incluido lo de libreria1h Cuando se incluye fichero2h se vuelve a intentar definir lo contenido en libreria1h y se obtiene un error de que esas definiciones estaacuten definidas dos veces

La forma de evitar este problema es meter todas las definiciones dentro de un bloque ifndef - endif con el nombre (_LIBRERIA_1_H en el ejemplo) que maacutes nos guste y distinto para cada uno de nuestros ficheros de cabecera Es habitual poner este nombre precedido de _ acabado en _H y que coincida con el nombre del fichero de cabecera pero en mayuacutesculas

Dentro del bloque ifndef - endif hacemos un define de ese nombre (no hace falta darle ninguacuten valor basta con que esteacute definido) y luego definimos todos nuestros tipos y prototipos de funciones

Cuando incluyamos este fichero por primera vez _LIBRERIA_1_H no estaraacute definido asiacute que se entraraacute dentro del bloque ifndef - endif y se definiraacuten todos los tipos y prototipos de funciones incluido el mismo _LIBRERIA_1_H Cuando lo incluyamos por segunda vez _LIBRERIA_1_H ya estaraacute definido (de la inclusioacuten anterior) por lo que no se entraraacute en el bloque ifndef - endif y no se redefiniraacute nada por segunda vez

Es buena costumbre hacer esto con todos nuestros h independientemente de que sean o no para libreriacuteas Si te fijas en alguacuten h del sistema veraacutes que tienes este tipo de cosas hasta aburrir Por ejemplo en usrincludestdioh lo primero que hay despueacutes de los comentarios es un ifndef _STDIO_H

Librerias estaacuteticas y dinaacutemicas

En linux podemos hacer dos tipos de libreriacuteas estaacuteticas y dinaacutemicas

Una libreriacutea estaacutetica es una libreriacutea que se copia en nuestro programa cuando lo compilamos Una vez que tenemos el ejecutable de nuestro programa la libreriacutea no sirve para nada (es un decir sirve para otros futuros proyectos) Podriacuteamos borrarla y nuestro programa seguiriacutea funcionando ya que tiene copia de todo lo que necesita Soacutelo se copia aquella parte de la libreriacutea que se necesite Por ejemplo si la libreriacutea tiene dos funciones y nuestro programa soacutelo llama a una soacutelo se copia esa funcioacuten

Una libreriacutea dinaacutemica NO se copia en nuestro programa al compilarlo Cuando tengamos nuestro ejecutable y lo estemos ejecutando cada vez que el coacutedigo necesite algo de la libreriacutea iraacute a buscarlo a eacutesta Si borramos la libreriacutea nuestro programa daraacute un error de que no la encuentra

iquestCuaacuteles son las ventajas e inconvenientes de cada uno de estos tipos de libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es maacutes grande ya que se hace copia de todo lo que necesita

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 33: Programacion de Socket en Linux

bull Un programa compilado con libreriacuteas estaacuteticas se puede llevar a otro ordenador sin necesidad de llevarse las libreriacuteas

bull Un programa compilado con libreriacuteas estaacuteticas es en principio maacutes raacutepido en ejecucioacuten Cuando llama a una funcioacuten de la libreriacutea la tiene en su coacutedigo y no tiene que ir a leer el fichero de la libreriacutea dinaacutemica para encontrar la funcioacuten y ejecutarla

bull Si cambiamos una libreriacutea estaacutetica a los ejecutables no les afecta Si cambiamos una dinaacutemica los ejecutables se ven afectados Esto es una ventaja si hemos cambiado la libreriacutea para corregir un error (se corrige automaacuteticamente en todos los ejecutables) pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo hemos antildeadido un paraacutemetro maacutes a una funcioacuten de la libreriacutea los ejecutables ya hechos dejan de funcionar)

iquestQueacute tipo de libreriacutea uso entonces

Es como siempre una cuestioacuten de compromiso entre las ventajas y los inconvenientes Para programas no muy grandes y por simplicidad yo suelo usar libreriacuteas estaacuteticas Las dinaacutemicas estaacuten bien para programas enormes o para libreriacuteas del sistema que como estaacuten en todos los ordenadores con linux no es necesario andar llevaacutendoselas de un lado a otro

En unix las libreriacuteas estaacuteticas suelen llamarse libnombrea y las dinaacutemicas libnombreso donde nombre es el nombre de nuestra libreriacutea

Compilar y enlazar con libreriacuteas estaacuteticas

Una vez que tenemos nuestro coacutedigo para conseguir una libreriacutea estaacutetica debemos realizar los siguientes pasos

bull Obtener los ficheros objeto (o) de todos nuestros fuentes (c) Para ello se compilan con cc -c fuentec -o fuenteo La opcioacuten -c le dice al compilador que no cree un ejecutable sino soacutelo un fichero objeto Aquiacute pongo el compilador cc porque es el que he usado para el ejemplo pero puede usarse gcc o el g++ (para C++) o uno de fortran pascal etc

bull Crear la libreriacutea (a) Para ello se usa el comando ar con los siguientes paraacutemetros ar -rv libnombrea fuente1o fuente2o La opcioacuten -r le dice al comando ar que tiene que insertar (o reemplazar si ya estaacuten dentro) los ficheros objeto en la libreriacutea La opcioacuten -v es verbose para que muestre informacioacuten mientras estaacute haciendo las cosas A continuacioacuten se ponen todos los fichero objeto que deseemos ar es en realidad un comando mucho maacutes geneacuterico que todo esto y sirve para empaquetar cualquier tipo de fichero (no soacutelo ficheros objeto) Tiene ademaacutes opciones para ver queacute ficheros hay dentro borrar algunos de ellos reemplazarlos etc

Hacer todo este proceso a mano cada vez puede ser un poco pesado Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde esteacuten los fuentes de la libreriacutea y utilizar make para compilarla Si no sabes de queacute estoy hablando eacutechale un

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 34: Programacion de Socket en Linux

ojo a la paginilla de los makefiles Afortunaacutedamente las reglas impliacutecitas de make ya saben hacer libreriacuteas estaacuteticas El fichero Makefile quedariacutea tan sencillo como esto

Makefile

CFLAGS=-Iltpath1gt -Iltpath2gt

libnombrea libnombrea (objeto1o ojbeto2o )

En CLAGS debes poner tantas opciones -Iltpathgt como directorios con ficheros h tengas que le hagan falta a los fuente de la libreriacutea para compilar La libreriacutea depende de los ficheros objetos que hay dentro de ella Eso se pone poniendo el nombre de la libreriacutea y entre pareacutentesis los ficheros objeto Hay algunas verisones de make que soacutelo admiten un fichero objeto dentro de los pareacutentesis Debe ponerse entonces

libnombrea libnombrea(objeto1o) libnombrea(objeto2o)

Ya tenemos la libreriacutea Ahora al compilar nuestro programa con el compilador debemos decirle doacutende estaacuten las libreriacuteas y cuales son La orden de compilacioacuten quedariacutea entonces

$ cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -llibreria1 -llibreria2

Los -Iltpathgt son para indicar doacutende estaacuten los ficheros de cabecera necesarios para la compilacioacuten (tanto propios del programa como los de nuestras libreriacuteas) Los -Lltpathgt son para indicar los directorios en los que se encuentran las libreriacuteas Los -llibreria son para indicar que se debe coger esa libreriacutea En el comando soacutelo ponemos libreriacutea El prefijo lib y la extensioacuten a ya la pone automaacuteticamente el compilador

Hay un detalle importante a tener en cuenta Las libreriacuteas deben ponerse de forma que primero esteacute la de maacutes alto nivel y al final la de maacutes bajo nivel Es decir tal cual lo tenemos en el ejemplo libreria1 puede usar funciones de libreria2 pero no al reveacutes El motivo es que al compilar se van leyendo las libreriacuteas consecutivamente y cargando de cada una de ellas soacutelo lo necesario Vamos a verlo con un ejemplo

Supongamos que miprogramao llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2 El compilador lee miprogramao Como este necesita funcion1 la apunta como necesaria Luego lee libreria1 Busca en las funciones necesarias encuentra funcion1 y la carga Como funcion1 llama a funcion2 apunta funcion2 como funcioacuten necesaria Luego lee libreria2 y como funcion2 es necesaria la carga Todo correcto

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 35: Programacion de Socket en Linux

Supongamos ahora que le hemos dado la vuelta al orden que hemos puesto -llibreria2 antes que -llibreria1 El compilador lee miprogramac Como este necesita funcion1 se apunta como necesaria Luego lee libreria2 Como funcion1 no es de esta libreria y no hay maacutes funciones necesarias (hasta ahora) ignora libreria2 y no carga nada de ella Luego lee libreria1 carga funcion1 y ve que esta necesita funcion2 Apunta funcion2 como necesaria pero ya se han acabado las librerias Se obitiene un error de linkado en el que dice que no encuentro funcion2

Esto nos dice tambieacuten que tenemos que tener un cierto orden a la hora de disentildear libreriacuteas Debemos hacerlas teniendo muy claro que unas pueden llamar a otras pero no las otras a las unas es decir organizarlas como en un arbol Las de arriba pueden llamar a funciones de las de abajo pero no al reveacutes

Existe una pequentildea trampa pero no es muy elegante Consiste en poner la misma libreriacutea varias veces en varias posiciones Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2 habriacutea compilado

Compilar y enlazar con libreriacuteas dinaacutemicas

Para compilar los mismos ficheros pero como libreriacutea dinaacutemica tenemos que seguir los siguientes pasos

bull Compilar los fuentes igual que antes para obtener los objetos bull Crear la libreriacutea con el comando ld Las opciones para este comando seriacutean ld -o

liblibreriaso objeto1o objeto2o -shared La opcioacuten -o liblibreriaso le indica el nombre que queremos dar a la libreriacutea La opcioacuten -shared le indica que debe hacer una libreriacutea y no un ejecutable (opcioacuten por defecto) objeto1o objeto2o son los ficheros objeto que queremos meter en la libreriacutea

Igual que antes hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make Al igual que antes si no sabes de que estoy hablando ahiacute tienes la paginilla de los makes Desgraciadamente las reglas impliacutecitas no saben hacer libreriacuteas dinaacutemicas (o al menos yo no he visto coacutemo) asiacute que tenemos que trabajar un poco maacutes en el Makefile Quedariacutea algo asiacute como

Makefile

liblibreriaso objeto1c objeto2c cc -c -o objeto1o objeto1c cc -c -o objeto2o objeto2c ld -o liblibreriaso objeto1o objeto2o -shared rm objeto1o objeto2o

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 36: Programacion de Socket en Linux

La libreriacutea depende de los fuentes Se compilan para obtener los o (habriacutea que antildeadir ademaacutes las opciones -Iltpathgt que fueran necesarias) se construye la libreriacutea con ld y se borran los objetos generados He hecho depender la libreriacutea de los fuentes para que se compile soacutelo si se cambia un fuente Si la hago depender de los objetos como al final los borro siempre se recompilariacutea la libreriacutea

El comando ld es maacutes especiacutefico que ar y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la libreriacutea No queda maacutes remedio que construir la libreriacutea entera cada vez que se modifique algo

Una vez generada la libreriacutea para enlazar con ella nuestro programa hay que poner

cc -o miprograma miprogramac -Iltpath1gt -Iltpath2gt -Lltpath1gt -Lltpath2gt -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las libreriacuteas estaacuteticas con la excepcioacuten del -Bdynamic Es bastante habitual generar los dos tipos de libreriacutea simultaacuteneamente con lo que es bastante normal encontrar de una misma libreriacutea su versioacuten estaacutetica y su versioacuten dinaacutemica Al compilar sin opcioacuten -Bdynamic puden pasar varias cosas

bull Existen liblibreriaa y liblibreriaso Se coge por defecto liblibreriaa bull Soacutelo existe una de ellas Se coge la que existe bull No existe ninguna de ellas Error

La opcioacuten -Bdynamic cambia el primer caso haciendo que se coja liblibreriaso en vez de liblibreriaa La opcioacuten -Bdynamic afecta a todas las libreriacuteas que van detraacuten en la liacutenea de compilacioacuten Para volver a cambiar podemos poner -Bstatic en cualquier momento

Una vez compilado el ejecutable nos falta un uacuteltimo paso Hay que decirle al programa mientras se estaacute ejecutando doacutende estaacuten las libreriacuteas dinaacutemicas puesto que las va a ir a buscar cada vez que se llame a una funcioacuten de ellas Tenemos que definir la variable de entorno LD_LIBRARY_PATH en la que ponemos todos los directorios donde haya libreriacuteas dinaacutemicas de intereacutes

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATHltpath1gtltpath2gtltpath3gt $ export LD_LIBRARY_PATH

Siendo ltpathgt los directorios en los que estaacuten las libreriacuteas dinaacutemicas Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y antildeadirle los nuevos directorios

iquestTe acuerdas del ejemplo del principio con la suma Aquiacute estaacuten todos los fuentes para que puedas jugar con ellos

bull sumac restac y libreria1h son los fuentes para la libreriacutea Descaacutergalos y quiacutetales la extensioacuten txt

bull principalc es el fuente para el programa principal que usa las funciones de la libreriacutea Descaacutergalo y quiacutetale la extensioacuten txt

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 37: Programacion de Socket en Linux

bull Makefile es un Makefile para generar todo Descaacutergalo y quiacutetale la extensioacuten txt Si haces make p1 se generaraacute la libreriacutea estaacutetica y se compilara principalc con la libreriacutea estaacutetica para generar un ejecutable p1 Si haces make p2 se generaraacute la libreriacutea dinaacutemica y se compilaraacute principalc con la libreriacutea dinaacutemica para generar un ejecutable p2 Si ejecutas p2 a pelo no funcionaraacute Acueacuterdate de poner el directorio actual (en el que se supone estaacute la libreriacutea dinaacutemica) en la variable de entorno LD_LIBRARY_PATH $ LD_LIBRARY_PATH= $ export LD_LIBRARY_PATH $ p2

En el Makefile hay algunas cosillas que he antildeadido respecto a lo explicado y las comento He puesto la opcioacuten de compilacioacuten -Wall para obtener todos los warning posibles Tambieacuten hay un objetivo clean que sirve para borrar las libreriacuteas y los ejecutables

SOCKETS UDP

Aquiacute suponemos que ya estaacuten claros los conceptos de lo que es una arquitectura clienteservidor un socket un servicio (fichero etcservices ) etc Si no es asiacute puedes leer el primer ejemplo antes de seguir leyendo en esta paacutegina

Los sockets UDP son sockets no orientados a conexioacuten Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en eacutel o leer sin necesidad de esperar a que alguien se conecte en el otro extremo del socket

El protocolo UDP al no ser orientado a conexioacuten no garantiza que el mensaje llegue a su destino Parece claro que si mi programa enviacutea un mensaje y no hay nadie escuchando ese mensaje se pierde De todas formas aunque haya alguien escuchando el protocolo tampoco garantiza que el mensaje llegue Lo uacutenico que garantiza es que si llega llega sin errores

iquestPara queacute sirve entonces Este tipo de sockets se suele usar para informacioacuten no vital por ejemplo enviacuteo de graacuteficos a una pantalla Si se pierde alguacuten graacutefico por el camino veremos que la pantalla pierde un refresco pero no es importante El que enviacutea los graacuteficos puede estar dedicado a cosas maacutes importantes y enviar los graacuteficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no

Otra ventaja es que con este tipo de sockets mi programa puede recibir mensajes de varios sitios a la vez Si yo estoy escuchando por un socket no orientado a conexioacuten cualquier otro programa en otro ordenador puede enviarme un mensaje Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez

El concepto de clienteservidor sigue teniendo aquiacute el mismo sentido El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo El cliente abre un socket UDP en cualquier serviciopuerto que esteacute libre y enviacutea un mensaje al servidor solicitando algo La diferencia principal con los TCP (orientados a conexioacuten) es

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 38: Programacion de Socket en Linux

que en estos uacuteltimos ambos sockets (de cliente y de servidor) estaacuten conectados y lo que escribimos en un lado sale por el otro En un UDP los sockets no estaacuten conectados asiacute que a la hora de enviar un mensaje hay que indicar quieacuten es el destinatario

En los sockets orientados a conexioacuten se enviacutean mensajes con write() o send() y se reciben con read() o recv() En un socket no orientado a conexioacuten hay que indicar el destinatario asiacute que se usan las funciones sendto() y recvfrom()

Veamos todo esto con coacutedigo y un ejemplo concreto

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes

bull Abrir un socket con la funcioacuten socket() bull Asociar el socket a un puerto con bind() bull Leer mensaje con recvfrom() bull Responder mensaje con sendto()

Veamos todo esto con un poco maacutes de detalle

Abrir el socket

Se abre el socket de la forma habitual con la funcioacuten socket() Esto siacutemplemente nos devuelve un descriptor de socket que todaviacutea no funciona ni es uacutetil La forma de llamarla seriacutea la siguiente

int DescriptorDescriptor = socket (AF_INET SOCK_DGRAM 0)

El primer paraacutemetro indica que es socket es de red (podriacutea ser AF_UNIX para un socket entre procesos dentro del mismo ordenador) El segundo indica que es UDP (SOCK_STREAM indicariacutea un socket TCP orientado a conexioacuten) El tercero es el protocolo que queremos utilizar Hay varios disponibles pero poniendo un 0 dejamos al sistema que elija este detalle

Asociar el socket con un puerto

Aunque ya se explicoacute en el ejemplo simple de socket damos aquiacute un pequentildeo repaso al tema del puertoservicio y el fichero etcservices

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles del 0 al 65535 Del 0 al 1023 estaacuten reservados para el sistema El resto estaacuten a nuestra disposicioacuten para lo que queramos Cuando abrimos un socket servidor debemos

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 39: Programacion de Socket en Linux

decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la funcioacuten bind()

Para decir que puerto queremos atender hay dos opciones

bull La primera es indicar directamente queacute nuacutemero queremos Esta opcioacuten no es buena puesto que nadie salvo nuestro programa sabraacute que estamos atendiendo ese puerto

bull La segunda es escribir en el fichero etcservices (con permisos de root) el nuacutemero de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de nuestro servicio Por ejemplo un servidor de paacuteginas web suele atender al puerto 80 y es normal encontrar en este fichero la siguiente liacutenea

www 80tcp http Worldwide Web HTTP

www es el nombre que damos al servicio y que deja bastante claro que es algo de internet Puede ser el nombre que queramos80 es el nuacutemero de servicio Este nuacutemero debe ser conocido por todos los navegadores que quieran acceder a este servidor de paacuteginas webtcp indica que este puerto es orientado a conexioacuten Para conectarnos desde un navegador a este puerto debemos usar un protocolo orientado a conexioacutenhttp es un segundo nombre que damos al servicioLo que va detraacutes de es un comentario en el fichero

En nuestro ejemplo hemos dado de alta en el fichero etcservices un servicio al que hemos llamado cpp_java que es udp y con un nuacutemero de puerto Es decir hemos antildeadido en dicho fichero una liacutenea como esta

cpp_java 25558udp servicio para programa de pruebas

Vamos ahora con los detalles del coacutedigo

Para decir al sistema operativo que deseamos atender a un determinado servicio de forma que cuando llegue un mensaje por ese servicio nos avise debemos llamar a la funcioacuten bind() La forma de llamarla es la siguiente

struct sockaddr_in Direccion Aquiacute hay que rellenar la estructura Direccion bind ( Descriptor (struct sockaddr )ampDireccion sizeof (Direccion))

El primer paraacutemetro es el descriptor de socket obtenido con la funcioacuten socket()

El segundo paraacutemetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra) Para los sockets que comunican procesos en la misma maacutequina AF_UNIX tenemos la estructura sockaddr_un que tambieacuten es compatible con sockaddr)

El tercer paraacutemetro es el tamantildeo de la estructura sockaddr_in

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 40: Programacion de Socket en Linux

iquest Coacutemo rellenamos la estructura sockaddr_in Hay tres campos que debemos rellenar

Direccionsin_family = AF_INETDireccionsin_port = Este campo se explica coacutemo rellenarlo un poco maacutes adelante Direccionsin_addrs_addr = INADDR_ANY

El campo sin_family se rellena con el tipo de socket que estamos tratando AF_INET en nuestro caso El campo s_addr es la direccioacuten IP del cliente al que queremos atender Poniendo INADDR_ANY atenderemos a cualquier cliente El campo sin_port es el nuacutemero de puertoservicio Para hacerlo bien debemos leer del fichero etcservices el nuacutemero del servicio cpp_java Para ello tenemos la funcioacuten getservbyname() La llamada a esta funcioacuten es de la siguiente manera

struct servent Puerto = NULLPuerto = getservbyname (Servicio udp)

Direccionsin_port = Puerto-gts_port

Se declara un puntero a la estructura servent La funcioacuten getservbyname() recibe el nombre del servicio y si es tcp o udp Busca en el fichero etcservices y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error) Una vez rellena la estructura servent tenemos en el campo s_port el nuacutemero del puerto Basta asignarlo a Direccionsin_port

Con esto el socket del servidor estaacute dispuesto para recibir y enviar mensajes

Recibir mensajes en el servidor

La funcioacuten para leer un mensaje por un socket upd es recvfrom() Esta funcioacuten admite seis paraacutemetros (funcioacuten compleja donde las haya) Vamos a verlos

bull int que es el descriptor del socket que queremos leer Lo obtuvimos con socket()bull char que es el buffer donde queremos que nos devuelva el mensaje Podemos

pasar cualquier estructura o array que tenga el tamantildeo suficiente en bytes para contener el mensaje Debemos pasar un puntero y hacer el cast a char

bull int que es el nuacutemero de bytes que queremos leer y que compondraacuten el mensaje El buffer pasado en el campo anterior debe tener al menos tantos bytes como indiquemos aquiacute

bull int con opciones de recepcioacuten De momento nos vale un 0bull struct sockaddr otra vez Esta vez tenemos suerte y no tenemos que rellenarla La

pasaremos vaciacutea y recvfrom() nos devolveraacute en ella los datos del que nos ha enviado el mensaje Si nos los guardamos luego podremos responderle con otro mensaje Si no queremos responder en este paraacutemetro podemos pasar NULL Ojo si lo hacemos asiacute no tenemos forma de saber quieacuten nos ha enviado el mensaje ni de responderle

bull int es el puntero a un entero En eacutel debemos poner el tamantildeo de la estructura sockaddr La funcioacuten nos lo devolveraacute con el tamantildeo de los datos contenidos en dicha estructura

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 41: Programacion de Socket en Linux

Nuestro coacutedigo para nuestro ejemplo quedariacutea para recibir un mensaje

struct sockaddr_in Cliente Contendraacute los datos del que nos enviacutea el mensaje int longitudCliente = sizeof(Cliente) Tamantildeo de la estructura anterior int buffer Nuestro mensaje es simplemente un entero 4 bytes

recvfrom (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente amplongtudCliente)

La funcioacuten se quedaraacute bloqueada hasta que llegue un mensaje Nos devolveraacute el nuacutemero de bytes leidos o -1 si ha habido alguacuten error

Responder con un mensaje al cliente

La funcioacuten para enviacuteo de mensajes es sendto() Esta funcioacuten admite seis paraacutemetros que son los mismos que la funcioacuten recvfrom() Su signifcado cambia un poco asiacute que vamos a verlos

bull int con el descriptor del socket por el que queremos enviar el mensaje Lo obtuvimos con socket()

bull char con el buffer de datos que queremos enviar En este caso al llamar a sendto() ya debe estar relleno con los datos a enviar

bull int con el tamantildeo del mensaje anterior en bytesbull int con opciones De momento nos vale poner un 0bull struct sockaddr Esta vez siacute tiene que estar relleno pero seguimos teniendo

suerte nos lo rellenoacute la funcioacuten recvfrom() Poniendo aquiacute la misma estructura que nos rellenoacute la funcioacuten recvfrom() estaremos enviando el mensaje al cliente que nos lo enviacuteo a nosotros previamente

bull int con el tamantildeo de la estructura sockaddr Vale el mismo entero que nos devolvioacute la funcioacuten recvfrom() como sexto paraacutemetro

Nuestro coacutedigo despueacutes de haber recibido el mensaje del cliente quedariacutea maacutes o menos

buffer = Rellenamos el mensaje de salida con los datos que queramos

sendto (Descriptor (char )ampbuffer sizeof(buffer) 0 (struct sockaddr )ampCliente longitudCliente)

La llamada enviacutea el mensaje y devuelve el nuacutemero de bytes escritos o -1 en caso de error

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes

bull Abrir un socket con socket() bull Asociar el socket a un puerto con bind()

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 42: Programacion de Socket en Linux

bull Enviar mensaje al servidor con sendto() bull Leer respuesta con recvfrom()

Abrir el socket

Exactamente igual que el servidor

Asociar el socket a un puerto

Igual que en el caso del servidor se hace con la funcioacuten bind() Hay pequentildeas diferencias en la forma de rellenar el segundo paraacutemetro (la estructura sockaddr) asiacute que las contamos aquiacute

Direccionsin_family = AF_INETDireccionsin_port = 0 Dejamos que el sistema elija el puerto uno libre cualquiera Direccionsin_addrs_addr = INADDR_ANY

Vemos que la diferencia es el campo sin_port Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera Esto se puede hacer asiacute porque el cliente no necesita tener un puerto conocido por el servidor Normalmente el cliente es el que comienza la comunicacioacuten pidieacutendole algo al servidor Cuando el mensaje de peticioacuten llega al servidor tambieacuten llega el puerto y maacutequina en la que estaacute el cliente con lo que el servidor podraacute responderle

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la funcioacuten es sendto() Los paraacutemetros y forma de funcionamiento es igual que en el servidor pero con la diferencia de que esta vez siacute que tenemos que rellenar la estructura sockaddr del quinto paraacutemetro La forma de hacerlo es la siguiente

Direccionsin_family = AF_INETDireccionsin_port = Aquiacute debemos dar el puerto del servidor el cpp_java del fichero etcservices Direccionsin_addrs_addr = Aquiacute debemos dar la direccioacuten IP del servidor en formato de red Lo vemos maacutes abajo

La forma de rellenar el campo sin_port ya nos la sabemos El servicio cpp_java debe estar en el fichero etcservices de la maacutequina donde corre el cliente con el mismo nuacutemero de puerto que se puso en la del servidor Se obtiene el puerto con la funcioacuten getservbyname() igual que se hizo en el bind() del servidor

La direccioacuten s_addr del servidor se debe obtener con la funcioacuten gethostbyname() En el fichero etchosts de la maacutequina hay una lista de nombres de maacutequinas conocidas y direcciones IP de las mismas (si esto te suena a chino te remito nuevamente al ejemplo simple) Para que gethostbyname() funcione correctamente se le pasa de paraacutemetro el

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 43: Programacion de Socket en Linux

nombre de una maacutequina que esteacute dada de alta en el fichero etchosts En nuestro ejemplo puesto que servidor y cliente van a correr en la misma maacutequina usaremos como nombre de maacutequina localhost que estaacute dado de alta por defecto en cualquier instalacioacuten de linux La llamada se hariacutea asiacute

struct hostent Maquina

Maquina = gethostbyname (localhost)Direccionsin_addrs_addr = ((struct in_addr )(Maquina-gth_addr))-gts_addr

Bueno la asignacioacuten parece un poco compleja Veamos de donde sale

La estructura Maquina tiene un campo h_addr de ahiacute la parte Maquina-gth_addr Este campo no es del tipo deseado asiacute que se convierte con un cast a struct in_addr de ahiacute lo de (struct in_addr )(Host-gth_addr) Bueno pues todo esto es a su vez una estructura de la que nos interesa el campo s_addr asiacute que metemos todo entre pareacutentesis cogemos el campo y nos queda lo que tenemos puesto en el coacutedigo ((struct in_addr )(Host-gth_addr))-gts_addr

Una vez rellena esta compleja estructura ya podemos enviar el mensaje al servidor igual que hicimos en eacutel con el mensaje sendto()

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes con la funcion recvfrom() La estructura sockaddr no hace falta rellenarla ya que nos la rellenaraacute la funcioacuten con los datos del servidor

EL EJEMPLO

Con todo esto vamos al ejemplo Vamos a hacer un servidor que espera recibir un entero de los clientes incrementa dicho entero y se lo devuelve incrementado Los fuentes de este ejemplo son Servidorc y Clientec

En nuestro ejemplo usamos la mini-libreriacutea que construimos para el uso de sockets Si quieres probar el ejemplo debes

1 Bajarte la libreriacutea ChSockettargz en un directorio descomprimirla y compilarla desde una shell de linux con$ make

2 Bajarte los fuentes Makefile Servidorc y Clientec en otro directorio distinto del de la libreriacutea y quitarles la extensioacuten txt

3 Cambiar la liacutenea del Makefile que pone DIRLIBSOCKET=LIBRERIAy poner tuacute el directorio donde te hayas bajado y compilado la libreriacuteaDIRLIBSOCKET=ltltMiDirectorioDeLibreriagtgt

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 44: Programacion de Socket en Linux

4 Compilar el ejemplo desde una shell de linux con$ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes puedes ejecutar el servidor con Servidor Desde otras shells puedes arrancar clientes con Cliente El cliente enviaraacute un nuacutemero aleatorio entre 0 y 19 al servidor y este se lo devolveraacute incrementado en 1

ALGUNAS DIFERENCIAS ENTRE PLATAFORMAS

En el mercado hay montones de microprocesadores y cada ordenador decide cual usa Los PC suelen usar Pentium o compatibles las estaciones de trabajo SUN tienen otro micro distinto (Sparc) los Mac otro (creo que PowerPC) etc etc

El problema es que cada micro de estos define los enteros los char etc etc como quiere Lo normal es que un entero por ejemplo sean cuatro bytes (32 bits) aunque algunos micros antiguos eran de 2 bytes (16 bits) y los maacutes modernos empiezan a ser de 8 bytes (64 bits)

Dentro de los de 4 bytes por ejemplo los Pentium hacen que el byte menos significativo ocupe la direccioacuten maacutes baja de memoria mientras que los Sparc por ejemplo lo ponen al reveacutes Es decir el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00 mientras que en Sparc o la maacutequina virtual de Java se representa como 00-00-00-01 Estas representaciones reciben el nombre de little endian (las de intel 80x86 Dec Vax y Dec Alpha) y big endian (IBM 360370 Motorola Sparc HP PA y la maacutequina virtual Java) Aquiacute httpoldalgoritmianetsoporteendianhtm tienes un enlace donde se cuenta esto con un poco maacutes de detalle

Si mandamos un entero a traveacutes de un socket estamos enviando estos cuatro bytes Si los micros a ambos lados del socket tienen el mismo orden para los bytes no hay problema pero si tienen distinto el entero se interpretaraacute incorrectamente

La maacutequina virtual Java por ejemplo define los char como de 2 bytes (para poder utilizar caracteres UNICODE) mientras que en el resto de los micros habituales suele ser de un byte Si enviamos desde Java un caraacutecter a traveacutes de un socket enviamos 2 bytes mientras que si lo leemos del socket desde un programa en C soacutelo leeremos un byte dejando el otro pendiente de lectura

De los float y double mejor no hablar Hay tambieacuten varios formatos y la conversioacuten de unos a otros no es tan faacutecil como dar la vuelta a cuatro bytes Suele ser buena idea si queremos comunicar maacutequinas distintas hacer que no se enviacuteen floats ni doubles

SOLUCIOacuteN

La solucioacuten a estos problemas pasa por enviar los datos de una forma maacutes o menos standard independiente del micro que tengamos

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 45: Programacion de Socket en Linux

Tal cual circulan por internet los enteros son de 4 bytes y van ordenados de la misma manera que Java o Sparc Los char son de 1 byte

Si estamos en un Pentium antes de enviar un entero por un socket hay que hacer el coacutedigo necesario para darle la vuelta a los bytes Cuando lo leemos debemos tambieacuten darle la vuelta a lo que hemos leido antes de utilizarlo Este coacutedigo soacutelo valdriacutea para un Pentium Si llevamos el coacutedigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc debemos borrar todo este coacutedigo de dar vuelta a los bytes

Afortunadamente tanto en C de linux como de windows (con winsocket) tenemos la familia de funciones htonl()

bull htonl() pasa un entero de formato hardware (el del micro) a formato red (Hardware TO Network)

bull ntohl() pasa un entero de formato red a formato hardware bull htons() hace lo mismo que htonl() pero con un short (16 bits) bull ntohs() hace lo mismo que ntohl() pero con un short

Estas funciones estaacuten implementadas para cada micro en concreto haciendo lo que sea necesario De esta forma si antes de enviar un entero por un socket llamamos a htonl() y depueacutes de leerlo del socket llamamos a ntohl() el entero circularaacute por la red en un formato estaacutendar y cualquier programa que lo tenga en cuenta seraacute capaz de leerlo

Llamando a estas funciones nuestro coacutedigo fuente es ademaacutes portable de una maacutequina a otra Bastaraacute recompilarlo En un Pentium estas funciones dan la vuelta a los bytes mientras que en una Sparc no hacen nada pero existen y compilan

En cuanto a los char puesto que Java es el uacutenico de momento que utiliza dos bytes en el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un uacutenico byte antes de enviar y los reconvierta a dos cuando los recibe La clase String de Java tiene meacutetodos que permiten hacer esto

EL COacuteDIGO C

Vamos a hacer un cliente y un servidor en C Cuando se conecten el cliente enviaraacute un entero indicando cuaacutentos caracteres van detraacutes (incluido un caracter nulo al final) y luego los caracteres Es decir enviaraacute por ejemplo un 5 y luego Hola y un caracter nulo Cuando lo reciba el servidor contestaraacute con un 6 y luego Adioacutes y un caracter nulo

Para este coacutedigo usaremos la mini-libreriacutea que hicimos en su momento Tambieacuten utilizaremos el servicio cpp_java que dimos de alta en su momento en el etcservices En el ejemplo hemos puesto a este servicio el puerto 25557

Al establecer la conexioacuten el servidor llamaraacute a la funcioacuten Abre_Socket_Inet() de la min-libreriacutea Se pasaraacute a esta funcioacuten como paraacutemetro en nombre del servicio cpp_java Luego se llamaraacute a la funcioacuten Acepta_Conexion_Cliente()

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 46: Programacion de Socket en Linux

int Socket_Servidor int Socket_Cliente Socket_Servidor = Abre_Socket_Inet (cpp_java) Socket_Cliente = Acepta_Conexion_Cliente (Socket_Servidor)

El cliente simplementa llamaraacute a la funcioacuten Abre_Conexion_Inet() pasaacutendole de paraacutemetros la maacutequina donde va a correr el servidor (localhost en nuestro caso) y el nombre del servicio al que se debe conectar (cpp_java en nuestro caso)

int Socket_Con_Servidor Socket_Con_Servidor = Abre_Conexion_Inet (localhost cpp_java)

En el interior de estas funciones hay un pequentildeo detalle En estas funciones internamente se utiliza una estructura struct sockaddr_in y dentro hay un campo llamado sin_port En este campo se coloca el nuacutemero de puertoservicio (25557 en nuestro caso) Si decidimos hacerlo como en el ejemplo es decir dar de alta el servicio en el fichero etcservices y leerlo con la funcioacuten getservbyname() no hay ninguacuten problema Sin embargo si decidimos en nuestro coacutedigo poner el nuacutemero a pelo hay que tener en cuenta que dicho nuacutemero va a circular por red El cliente enviaraacute al servidor este nuacutemero de puerto para indicarle a queacute puerto conectarse Este nuacutemero por tanto debe circular por la red en formato red Resumiento que antes de meterlo en el campo sin_port hay que darle la vuelta a los bytes Como el campo es un short se usaraacute la funcioacuten htons() El coacutedigo seriacutea maacutes o menos

struct sockaddr_in Direccion Direccionsin_port = htons (25557)

Todo esto repito seriacutea si no utilizamos las funciones de la mini-libreria que suponen que el servicio estaacute de alta en etcservices y que utilizan la funcioacuten getservbyname()

Tanto en cliente como servidor ara el enviacuteo de datos usaremos la funcioacuten Escribe_Socket() que lleva varios paraacutemetros Antes de enviar un entero debemos convertirlo a formato red Por ejemplo en el cliente tenemos el siguiente coacutedigo

int Longitud_Cadena int Aux Longitud_Cadena = 6 Aux = htonl (Longitud_Cadena) Se mete en Aux el entero en formato red Se enviacutea Aux que ya tiene los bytes en el orden de red Escribe_Socket (Socket_Con_Servidor (char )ampAux sizeof(Longitud_Cadena))

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 47: Programacion de Socket en Linux

char Cadena[100] strcpy (Cadena Adios) Escribe_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

En cuanto a la lectura se usaraacute la funcioacuten Lee_Socket() A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcioacuten ntohl() El coacutedigo para el cliente seriacutea

int Longitud_Cadena int Aux Lee_Socket (Socket_Con_Servidor (char )ampAux sizeof(int)) La funcioacuten nos devuelve en Aux el entero leido en formato red Longitud_Cadena = ntohl (Aux) Guardamos el entero en formato propio en Longitud_Cadena Ya podemos leer la cadena char Cadena[100] Lee_Socket (Socket_Con_Servidor Cadena Longitud_Cadena)

Nada maacutes por la parte de C Como se puede ver el uacutenico truco consiste en llamar a la funcioacuten htonl() antes de enviar un entero por el socket y a la funcioacuten ntohl() despueacutes de leerlo

Puedes ver el coacutedigo en Servidorc y Clientec Para compilarlo tines que quitarles la extensioacuten txt descargarte el Makefile (tambieacuten le quitas el txt) y la mini-libreria En el Makefile cambia PATH_CHSOCKET para que apunte al directorio donde hayas descargado y compilado la mini-libreria Luego compila desde una shell de linux con

$ make

EL COacuteDIGO JAVA

El servidor y cliente que haremos en java haraacuten exactamente lo mismo que los de C De esta forma podremos arrancar cualquiera de los dos servidores (el de C o el de java) con cualquiera de los dos clientes (el de C o el de java) y deberiacutean funcionar igual

En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente Puedes ver coacutemo se usan en el ejemplo de sockets en java

Los datos a enviar los encapsulamos en una clase DatoSocket Esta clase contendraacute dos atributos un entero que es la longitud de la cadena (sin incluir el nulo del final aunque podemos decidir lo contrario) y un String que es la cadena a enviar

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 48: Programacion de Socket en Linux

class DatoSocket public int c public String d

Puesto que no podemos enviar el objeto tal cual por el socket puesto que C no lo entenderiacutea debemos hacer un par de meacutetodos que permitan enviar y recibir estos dos atributos de un socket al estilo C (formato red standard) Dentro de la misma clase DatoSocket hacemos el meacutetodo public void writeObject(javaioDataOutputStream out) que nos permite escribir estos dos atributos por un stream

public void writeObject(javaioDataOutputStream out) throws IOException Se enviacutea la longitud de la cadena + 1 por el 0 necesario en C outwriteInt (c+1)

Se enviacutea la cadena como bytes outwriteBytes (d)

Se enviacutea el 0 del final outwriteByte (0)

Tambieacuten hacemos el meacutetodo public void readObject(javaioDataInputStream in) que nos permita leer los atributos de un stream

public void readObject(javaioDataInputStream in) throws IOException Se lee la longitud de la cadena y se le resta 1 para eliminar el 0 que nos enviacutea C c = inreadInt() - 1

Array de bytes auxiliar para la lectura de la cadena byte [] aux = null aux = new byte[c] Se le da el tamantildeo

inread(aux 0 c) Se leen los bytes d = new String (aux) Se convierten a String

Se lee el caracter nulo del final inread(aux01)

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra

Page 49: Programacion de Socket en Linux

Estos meacutetodos no son robustos Deberiacuteamos hacer varias comprobaciones del estilo si nos envian una longitud negativa o cero no deberiacuteamos leer la cadena etc etc

Tanto en el coacutedigo del cliente como en el del servidor cuando queramos enviar estos datos constuiremos un DataOutputStream y llamaremos al meacutetodo writeObject() de la clase DatoSocket Por ejemplo en nuestro servidor si cliente es el Socket con el servidor

DatoSocket dato = new DatoSocket(Hola) El dato a enviar DataOutputStream bufferSalida = new DataOutputStream (clientegetOutputStream()) Se construye el stream de salida datowriteObject (bufferSalida) Se envia el dato

Para la lectura lo mismo pero construyendo un DataInputStream En nuestro servidor tendremos

DataInputStream bufferEntrada = new DataInputStream (clientegetInputStream()) Se contruye el stream de entrada DatoSocket aux = new DatoSocket() Para guardar el dato leido del socket auxreadObject (bufferEntrada) Se lee del socket

Nada maacutes en la parte de java Los fuentes son SocketServidorjava SocketClientejava y DatoSocketjava Puedes bajartelos quitarles la extensioacuten txt y compilarlos desde una shell de unix con

$ javac SocketServidorjava SocketClientejava DatoSocketjava

Ahora puedes ejecutar por ejemplo el servidor java con el cliente C y deberiacutea funcionar al igual que el servidor C con el cliente java o cualquier combinacioacuten servidor-cliente que se te ocurra