You are on page 1of 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
estén corriendo en ordenadores distintos. De hecho, con C en Linux/Unix tenemos una
serie de funciones que nos permiten enviar y recibir datos de otros programas, en C o en
otros lenguajes de programación, que estén corriendo en otros ordenadores de la misma
red.

En este artículo no se pretende dar una descripción detallada y rigurosa del protocolo
TCP/IP y lo que va alrededor de él. Simplemente se darán unas nociones básicas de
manera informal, con la intención de que se pueda comprender un pequeño ejemplo de
programación en C.

Está, por tanto, orientado a personas que tengan unos conocimientos básicos de C en
Linux y deseen o necesiten comunicar dos programas en C que corren simultáneamente
en dos ordenadores distintos conectados en red. El ejemplo propuesto puede servir
como guía 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
TCP/IP, es la programación de sockets. Un socket no es más que un "canal de
comunicación" entre dos programas que corren sobre ordenadores distintos o incluso en
el mismo ordenador.

Desde el punto de vista de programación, un socket no es más que un "fichero" que se


abre de una manera especial. Una vez abierto se pueden escribir y leer datos de él con
las habituales funciones de read() y write() del lenguaje C. Hablaremos de todo esto con
detalle más adelante.

Existen básicamente dos tipos de "canales de comunicación" o sockets, los orientados a


conexión y los no orientados a conexión.

En el primer caso ambos programas deben conectarse entre ellos con un socket y hasta
que no esté establecida correctamente la conexión, ninguno de los dos puede transmitir
datos. Esta es la parte TCP del protocolo TCP/IP, y garantiza que todos los datos van a
llegar de un programa al otro correctamente. Se utiliza cuando la información a
transmitir es importante, no se puede perder ningún dato y no importa que los
programas se queden "bloqueados" esperando o transmitiendo datos. Si uno de los
programas está atareado en otra cosa y no atiende la comunicación, el otro quedará
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 esté "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 está controlando la temperatura de un
horno y envía dicha temperatura a un ordenador en una sala de control para que éste
presente unos gráficos de temperatura. Obviamente es más importante el control del
horno que el perfecto refresco de los gráficos.
En el ejemplo y a partir de este momento nos referimos únicamente a sockets TCP. Los
UDP son básicamente iguales, aunque hay pequeñas diferencias en la forma de abrirlos.

ARQUITECTURA CLIENTE / SERVIDOR

A la hora de comunicar dos programas, existen varias posibilidades para establecer la


conexión inicialmente. Una de ellas es la utilizada aquí. Uno de los programas debe
estar arrancado y en espera de que otro quiera conectarse a él. Nunca da "el primer
paso" en la conexión. Al programa que actúa de esta forma se le conoce como servidor.
Su nombre se debe a que normalmente es el que tiene la información que sea disponible
y la "sirve" al que se la pida. Por ejemplo, el servidor de páginas web tiene las páginas
web y se las envía 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 información al servidor. El navegador de Internet pide la
página web al servidor de Internet.

En este ejemplo, el servidor de páginas web se llama servidor porque está (o debería de
estar) siempre encendido y pendiente de que alguien se conecte a él y le pida una
página. El navegador de Internet es el cliente, puesto que se arranca cuando nosotros lo
arrancamos y solicita conexión con el servidor cuando nosotros escribimos, por
ejemplo, www.google.com

En el juego del Quake, debe haber un servidor que es el que tiene el escenario del juego
y la situación de todos los jugadores en él. 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 conexión con él, normalmente, para pedirle algún dato. Cliente es el programa
que solicita la conexión para, normalmente, pedir datos al servidor.

LA CONEXIÓN

Para poder realizar la conexión entre ambos programas son necesarias varias cosas:

• Dirección IP del servidor.


Cada ordenador de una red tiene asignado un número único, que sirve para
identificarle y distinguirle de los demás, de forma que cuando un ordenador
quiere hablar con otro, manda la información a dicho número. Es similar a
nuestros números de teléfono. Si quiero hablar con mi amigo "Josechu", primero
marco su número de teléfono y luego hablo con él.

El servidor no necesita la dirección de ninguno de los dos ordenadores, al igual


que nosotros, para recibir una llamada por teléfono, no necesitamos saber el
número de nadie, ni siquiera el nuestro. El cliente sí necesita saber el número del
servidor, al igual que nosotros para llamar a alguien necesitamos saber su
número de teléfono.

La dirección IP es un número del estilo 192.100.23.4. ¡Todos lo hemos visto en


Internet!. En resumidas cuentas, el cliente debe conocer a qué ordenador desea
conectarse. En nuestro navegador de Internet facilitamos la dirección IP del
servidor al que queremos conectarnos a través de su nombre (www.google.com).
Obviamente este nombre hay que traducirlo a una dirección IP, pero nuestro
navegador e Internet se encargan de eso por nosotros.

• Servicio que queremos crear / utilizar.

Si llamamos a una empresa, puede haber en ella muchas personas, cada una con
su extensión de teléfono 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 información,
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 páginas web
corriendo a la vez.

Cuando un cliente desea conectarse, debe indicar qué servicio quiere, igual que
al llamar a la empresa necesitamos decir la extensión 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 número único que lo
identifique (como la extensión de teléfono). Estos números son enteros normales
y van de 1 a 65535. Los número bajos, desde 1 a 1023 están reservados para
servicios habituales de los sistemas operativos (www, ftp, mail, ping, etc). El
resto están a disposición del programador y sirven para cosas como Quake.

Tanto el servidor como el cliente deben conocer el número del servicio al que
atienden o se conectan, ya que el sistema operativo es más torpe que la
telefonista. El sistema operativo pedirá al cliente la "extensión" (no le vale el
nombre del servicio) y "cantará el número en voz alta" para ver qué persona de
la empresa atiende a ese número. Tanto el que llama como el que le debe atender
se tienen que saber el número.
En el caso del navegador de Internet, estamos indicando el servicio con la www
en www.google.com, servicio de páginas web. También es posible, por ejemplo
ftp.google.com, si google.com admite clientes ftp. Nuestro ordenador es lo
suficientemente listo como para saber a qué número corresponden esos servicios
habituales.

EL SERVIDOR

A partir de este punto comenzamos con lo que es la programación en C de los sockets.


Si no tienes unos mínimos conocimientos de C, es mejor que los adquieras antes de
seguir.

Con C en Unix/Linux, los pasos que debe seguir un programa servidor son los
siguientes:

• Apertura de un socket, mediante la función socket(). Esta función devuelve un


descriptor de fichero normal, como puede devolverlo open(). La función
socket() no hace absolutamente nada, salvo devolvernos y preparar un descriptor
de fichero que el sistema posteriormente asociará a una conexión en red.
• Avisar al sistema operativo de que hemos abierto un socket y queremos que
asocie nuestro programa a dicho socket. Se consigue mediante la función bind().
El sistema todavía no atenderá a las conexiones de clientes, simplemente anota
que cuando empiece a hacerlo, tendrá que avisarnos a nosotros. Es en esta
llamada cuando se debe indicar el número de servicio al que se quiere atender.
• Avisar al sistema de que comience a atender dicha conexión de red. Se consigue
mediante la función listen(). A partir de este momento el sistema operativo
anotará la conexión de cualquier cliente para pasárnosla cuando se lo pidamos.
Si llegan clientes más rápido de lo que somos capaces de atenderlos, el sistema
operativo hace una "cola" con ellos y nos los irá pasando según vayamos
pidiéndolo.
• Pedir y aceptar las conexiones de clientes al sistema operativo. Para ello
hacemos una llamada a la función accept(). Esta función le indica al sistema
operativo que nos dé al siguiente cliente de la cola. Si no hay clientes se quedará
bloqueada hasta que algún cliente se conecte.
• 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 qué datos esperan recibir,
qué datos deben enviar y en qué formato. Puedes ver cómo se pueden poner de
acuerdo en estos mensajes en el apartado de mensajes.
• Cierre de la comunicación y del socket, por medio de la función close(), que es
la misma que sirve para cerrar un fichero.

EL CLIENTE

Los pasos que debe seguir un programa cliente son los siguientes:

• Apertura de un socket, como el servidor, por medio de la función socket()


• Solicitar conexión con el servidor por medio de la función connect(). Dicha
función quedará bloqueada hasta que el servidor acepte nuestra conexión o bien
si no hay servidor en el sitio indicado, saldrá dando un error. En esta llamada se
debe facilitar la dirección IP del servidor y el número de servicio que se desea.
• Escribir y recibir datos del servidor por medio de las funciones write() y read().
• Cerrar la comunicación por medio de close().

FICHEROS UNIX/LINUX IMPLICADOS

Para la programación de socket no es estrictamente necesario ningún fichero. Sabiendo


la dirección IP y el número de servicio, se ponen directamente en código y todo
resuelto. Sin embargo, esto no es lo más cómodo ni lo más portable de unos
ordenadores a otros.

Hay dos ficheros en Unix/Linux que nos facilitan esta tarea, aunque hay que tener
permisos de root para modificarlos. Estos ficheros serían el equivalente a una agenda de
teléfonos, en uno tenemos apuntados el nombre de la empresa con su número de
teléfono y en el otro fichero el nombre de la persona y su extensión (EmpresaGorda,
tlfn 123.456.789; JoseGordo, extensión 2245; ...)

/etc/hosts : Esta es la agenda en la que tenemos las empresas y sus números de teléfono.
En este fichero hay una lista de nombres de ordenadores conectados en red y dirección
IP de cada uno. Habitualmente en el /etc/hosts del cliente se suele colocar el nombre del
servidor y su dirección IP. Luego, desde programa, se hace una llamada a la función
gethostbyname(), a la que pasándole el nombre del ordenador como una cadena de
caracteres, devuelve una estructura de datos entre los que está la dirección IP.

Una línea de lo que puede aparecer en este fichero es la siguiente, en el que vemos la
dirección IP y el nombre del ordenador que nos da el servicio de Quake.

192.30.10.1 Ordenador_Quake

/etc/services : Este fichero es el equivalente a la agenda donde tenemos apuntados los


distintos departamentos/personas de la empresa y sus números de extensión telefónica.
En este fichero hay una lista de servicios disponibles, indicando nombre de servicio,
número de servicio y tipo (ftp/udp).

Tanto el servidor como el cliente deben tener en este fichero el servicio que están
atendiendo / solicitando con el mismo número 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 función
getservbyname(), a la que pasándole el nombre del servicio, devuelve una estructura de
datos entre los que está el número de servicio y el tipo.

Un ejemplo de lo que puede aparecer en un fichero /etc/services es el siguiente, en el


que vemos en cada línea nombre del servicio, número / tipo y un comentario opcional
detrás del símbolo #. "Casualmente", se ve el servicio www, cuyo número de servicio
conocido por todos los ordenadores de Internet, es el 80.

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

EJEMPLO

Sentadas las bases de los sockets, vamos a ver un pequeño ejemplo.zip de programa
servidor y cliente, realizado con C en Linux. El servidor esperará la conexión del
cliente. Una vez conectados, se enviarán una cadena de texto el uno al otro y ambos
escribirán 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 revés.

DETALLES DEL SERVIDOR

En este apartado vamos a detallar las llamadas a las funciones del servidor que
indicamos anteriormente. Explicaremos con cierto detalle qué parámetros se deben
pasar y cual es el resultado de dichas llamadas.

En primer lugar el servidor debe obtener el número del servicio al que quiere atender,
haciendo la llamada a getservbyname(). Esta función devuelve una estructura (en
realidad puntero a la estructura) en el que uno de sus campos contiene el número de
servicio solicitado.

struct servent Puerto; /* Estructura devuelta */

/* La llamada a la función */
Puerto = getservbyname ("Nombre_Servicio", "tcp");

Los parámetros de la función son dos cadenas de caracteres. La primera es el nombre


del servicio, tal cual lo escribimos en el fichero /etc/services. El segundo es el tipo de
protocolo que queremos usar. "tcp" o "udp", El número de servicio que nos interesa está
en un campo de Puerto:

Puerto->s_port
Ya tenemos todos los datos que necesita el servidor para abrir el socket, así que
procedemos a hacerlo. El socket se abre mediante la llamada a la función socket() y
devuelve un entero que es el descriptor de fichero o –1 si ha habido algún error.

int Descriptor;
Descriptor = socket (AF_INET, SOCK_STREAM, 0);
if (Descriptor == -1)
printf ("Error\n");

El primer parámetro 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 sólo para el caso de que el cliente corra en
el mismo ordenador que el servidor, pero lo implementa de forma más eficiente. Si
ponemos AF_UNIX, el resto de las funciones varía ligeramente.

El segundo parámetro indica si el socket es orientado a conexión (SOCK_STREAM) o


no lo es (SOCK_DGRAM). De esta forma podremos hacer sockets de red o de Unix de
ambos tipos.

El tercer parámetro 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 función bind(). El
problema de la función bind() es que lleva un parámetro bastante complejo que
debemos rellenar.

struct sockaddr_in Direccion;


Direccion.sin_family = AF_INET;
Direccion.sin_port = Puerto->s_port;
Direccion.sin_addr.s_addr =INADDR_ANY;

if (bind (Descriptor, (struct sockaddr *)&Direccion, sizeof


(Direccion)) == -1)
{
printf ("Error\n");
}

El parámetro que necesita es una estructura sockaddr. Lleva varios campos, entre los
que es obligatorio rellenar los indicados en el código.

sin_family es el tipo de conexión (por red o interna), igual que el primer parámetro de
socket().
sin_port es el número correspondiente al servicio que obtuvimos con getservbyname().
El valor está en el campo s_port de Puerto.
Finalmente sin_addr.s_addr es la dirección del cliente al que queremos atender.
Colocando en ese campo el valor INADDR_ANY, atenderemos a cualquier cliente.

La llamada a bind() lleva tres parámetros:

• Descriptor del socket que hemos abierto


• Puntero a la estructura Direccion con los datos indicados anteriormente. La
estructura admitida por este parámetro 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
parámetro, debemos hacer un "cast" al tipo struct sockaddr. La operación cast es
un pequeño 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 paréntesis y detrás la variable del
tipo no deseado. Por ejemplo, para convertir de entero a flotante:

Variable_Flotante = (float)Variable_Entera;
Esto es válido siempre y cuando los dos tipos tengan algún tipo de relación 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.

• Longitud de la estructura Direccion.

La función 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 función listen()

if (listen (Descriptor, 1) == -1)


{
printf ("Error\n");
}

La función listen() admite dos parámetros:

• Descriptor del socket.


• Número máximo de clientes encolados. Supongamos que recibimos la conexión
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
código necesario para atender al primero. Mientras sucede todo esto, llega un
tercer cliente que también se conecta. Estamos atendiendo al primero y tenemos
dos en la "lista de espera". El segundo parámetro de listen() indica cuántos
clientes máximo 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 está llena, el nuevo cliente que llama a connect()
recibirá un error de dicha función.

La función listen() devuelve –1 en caso de error.

Con todo esto ya sólo queda recoger los clientes de la lista de espera por medio de la
función accept(). Si no hay ningún cliente, la llamada quedará bloqueada hasta que lo
haya. Esta función 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 sólo sirve para
encolar a los clientes. Digamos que el primer descriptor es el aparato de teléfono de la
telefonista de la empresa y el segundo descriptor es el aparato de teléfono del que está
atendiendo al cliente.
struct sockaddr Cliente;
int Descriptor_Cliente;
int Longitud_Cliente;

Descriptor_Cliente = accept (Descriptor, &Cliente, &Longitud_Cliente);


if (Descriptor_Cliente == -1)
{
printf ("Error\n");
}

La función accept() es otra que lleva un parámetro complejo, pero que no debemos
rellenar. Los parámetros que admite son

• Descriptor del socket abierto.


• Puntero a estructura sockaddr. A la vuelta de la función, esta estructura
contendrá la dirección y demás datos del ordenador cliente que se ha conectado
a nosotros.
• Puntero a un entero, en el que se nos devolverá la longitud útil del campo
anterior.

La función devuelve un –1 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 haría con un fichero. Supongamos
que sabemos que al conectarse un cliente, nos va a mandar una cadena de cinco
caracteres. Para leerla sería

int Leido = 0;/* Número de caracteres leídos hasta el momento */


int Aux = 0; /* Guardaremos el valor devuelto por read() */
int Longitud = 5; /* Número de caracteres a leer */
char Datos[5]; /* Buffer donde guardaremos los caracteres */

/* Bucle hasta que hayamos leído todos los caracteres que


estamos esperando */
while (Leido < Longitud)
{
/* Se leen los caracteres */
Aux = read (Descriptor, Datos + Leido, Longitud - Leido);

/* Si hemos conseguido leer algún carácter */


if (Aux > 0)
{
/* Se incrementa el número de caracteres leídos */
Leido = Leido + Aux;
}
else
{
/* Si no se ha leído ningún carácter, se comprueba
la condición de socket cerrado */
if (Aux == 0)
{
printf ("Socket cerrado\n");
}
else
/* y la condición de error */
if (Aux == -1)
{
printf ("Error\n");
}
}
}

Parece un poco complicado. A ver si somos capaces de aclararlo. La función read()


admite como parámetros

• Descriptor del fichero / socket del que se quiere leer


• Puntero a char donde se almacenarán los datos leídos
• Número de caracteres que se quieren leer.

La función devuelve –1 si ha habido error, 0 si el socket se ha cerrado o el fichero ha


llegado a fin de fichero, o bien el número 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 pequeño problema añadido 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 leído y cuantos faltan.

Por este motivo, read() va dentro de un while() en el que se mira si el total de caracteres
leídos es menor que el número de caracteres que queremos leer. La lectura del read()
debe además pasar como parámetro cada vez la posición dentro del buffer de lectura en
la que queremos situar los caracteres (Datos + Leido ) y la longitud de caracteres a leer,
(Longitud - Leido ) que también cambia según se va leyendo.

En cuanto a escribir caracteres, la función write() admite los mismos parámetros, con la
excepción de que el buffer de datos debe estar previamente relleno con la información
que queremos enviar. Tiene el mismo problema, que escribe lo que puede y vuelve,
pudiendo no haber escrito toda la información, por lo que hay que hacer un trozo de
código similar.

int Escrito = 0; /* Núm. de caracteres que ya se han escrito */


int Aux = 0; /* Número de caracteres escritos en cada pasada */
int Longitud = 5; /* Total de caracteres a escribir */
char Datos[] = "Hola"; /* El 5º carácter es el \0 del final */

/* Bucle mientras no se hayan escrito todos los caracteres


deseados */
while (Escrito < Longitud)
{
/* Se escriben los caracteres */
Aux = write (Descriptor, Datos + Escrito, Longitud - Escrito);

/* Si hemos conseguido escribir algún carácter */


if (Aux > 0)
{
/* Incrementamos el número de caracteres escritos */
Escrito = Escrito + Aux;
}
else
{
/* Si no hemos podido escribir caracteres,
comprobamos la condición de socket cerrado */
if (Aux == 0)
{
printf ("Socket cerrado\n");
}
else
/* y la condición de error */
if (Aux == -1)
{
printf ("Error\n");
}
}
}

En un programa más serio, ni el cliente ni el servidor saben a priori qué 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
qué mensaje va a llegar y su longitud, se procede a leerlo. A la hora de escribir, primero
se manda la cabecera diciendo qué mensaje se va a mandar y qué longitud tiene, y luego
se manda el mensaje en sí.

Una vez que se han leído / enviado todos los datos necesarios, se procede a cerrar el
socket. Para ello se llama a la función close(), que admite como parámetro 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
señor de una empresa nos ha atendido le dice a la telefonista que no atienda más el
teléfono. Esas cosas sólo las hacen los jefes.

DETALLES DEL CLIENTE

Puesto que la escritura / lectura y cierre de sockets es idéntica a la del servidor,


únicamente contaremos la apertura del socket que, cómo no, es más compleja que la del
servidor, puesto que además del número de servicio, debe obtener la dirección IP del
servidor.

En primer lugar, como en el servidor, obtenemos el número del servicio.

struct servent *Puerto;


Puerto = getservbyname ("Nombre_Servicio", "tcp");
if (Puerto == NULL)
{
printf ("Error\n");
}

Después obtenemos la dirección IP del servidor. El parámetro que hay


que pasar es una cadena de caracteres con el nombre del servidor, tal
cual lo pusimos en el fichero /etc/hosts. Devuelve los datos del
servidor en una estructura hostent.

struct hostent *Host;

Host = gethostbyname ("Nombre_Servidor");


if (Host == NULL)
{
printf ("Error\n");
}

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 ("Error\n");
}

Ya tenemos todo lo necesario para solicitar conexión con el servidor. El parámetro de


connect() ya es conocido y sólo tenemos que rellenarlo, igual que en bind() del servidor.

struct sockaddr_in Direccion;


Direccion.sin_family = AF_INET;
Direccion.sin_addr.s_addr = ((structin_addr*)(Host->h_addr))->s_addr;
Direccion.sin_port = Puerto->s_port;

if (connect (Descriptor, (struct sockaddr *)&Direccion,sizeof


(Direccion)) == -1)
{
printf ("Error\n");
}

La única novedad es que la dirección del servidor se coloca en


Direccion.sin_addr.s_addr y no como pusimos antes, INADDR_ANY. Dicha dirección
la tenemos dentro de la estructura obtenida con gethostbyname(), en el campo
((structin_addr*)(Host->h_addr))->s_addr. ¡Vaya por Dios!, ¡Menudo 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 explicación es la siguiente:

La estructura Host tiene un campo h_addr, de ahí la parte Host->h_addr.

Este campo no es del tipo deseado, así que se convierte con un cast a struct in_addr, de
ahí lo de (struct in_addr*)(Host->h_addr).
Bueno, pues todo esto es a su vez una estructura, de la que nos interesa el campo
s_addr, así que metemos todo entre paréntesis, cogemos el campo y nos queda lo que
tenemos puesto en el código ((structin_addr*)(Host->h_addr))->s_addr

El resto es exactamente igual que en el servidor.

ALGUNAS CONSIDERACIONES

Puede haber variaciones con otro tipo de sockets.

El ejemplo aquí expuesto funciona correctamente, pero si se quieren hacer otras cosas
un poco más serias hay que tener en cuenta varios puntos que indicamos aquí por
encima.

En primer lugar el ejemplo es con sockets orientados a conexión 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 conexión, la idea básica es la misma, pero la sintaxis
del código cambia ligeramente. Antes de realizar dicho cambio, conviene saber qué
cosas hay que cambiar en el código. Un ejemplo claro, si usamos sockets no orientados
a conexión, 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 aquí un
ejemplo de socket udp.

Si usamos sockets AF_UNIX, sobra pedir la dirección IP del servidor, ya que es el


mismo ordenador en el que corren ambos programas.

Organización de los enteros.

En el mundo de los ordenadores, están los micros de intel (y los que los emulan) y los
demás. La diferencia, entre otras muchas, es que organizan los enteros de forma
distinta; uno pone antes el byte más 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 conexión es un entero con el número de servicio. ¡Ya la hemos liado! hay micros que
no se entienden entre sí. Para evitar este tipo de problemas están la funciones htons(),
htonl() y similares. Esta función 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 función antes de realizar la conexión. Si enviamos después 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 máquina virtual, por lo que es
como si corriera en su propio microprocesados, distinto de los pentium.

Atención al cliente

Una técnica habitual en el servidor es que cree nuevos procesos (o hilos) para cada
cliente. Puedes echar un ojo a la función fork().Si no se quieren crear procesos, se puede
usar la función select(). A esta función 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 rápido 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 rápido a cada uno, basta con una única ventanilla y una persona en ella que
"duerma disimuladamente" mientras llega un cliente. Si los clientes llegan a mogollón y
se tarda en atender a cada uno de ellos, es mejor que haya muchas ventanillas, lo ideal,
una por cada cliente.

BIBLIOGRAFÍA

Un excelente libro para programación avanzada en C sobre entorno Unix es "UNIX,


Programación avanzada" de Fco. Manuel Márquez García, editorial ra-ma.

Tienes una guia bastante simple, pero más detallada de sockets en


http://www.arrakis.es/~dmrq/beej/index.html
Inet (char *) Abre un socket AF_INET. Este tipo de socket
sí sale por red y permite comunicar
programas que se ejecuten en distintos
ordenadores. Se le pasa un nombre de
servicio tcp que esté dado de alta en el fichero
/etc/services. Devuelve el descriptor del
socket servidor y debemos guardárnoslo. -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.
Abre un socket UDP para atender el servicio
cuyo nombre se le pasa como parámetro. El
int Abre_Socket_Udp (char servicio debe estar dado de alta en
*) /etc/servicies como udp. La función
devuelve el descriptor del socket si todo ha
ido bien o -1 en caso de error.

CLIENTE

int Abre_Conexion_Unix (char *) Establede una conexión AF_UNIX con el


servidor. Se le pasa el mismo fichero que se
pasó en el servidor a la función
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 conexión AF_INET con el
servidor. Se le pasa el mismo nombre de
servicio que se le pasó al servidor en la función
Abre_Socket_Inet() y que está dado de alta en
el fichero /etc/services. 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 servicio/puerto que esté libre.
La función nos devuelve el descriptor del socket
o -1 si ha habido algún error.

COMUNES

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

• el descriptor obtenido con la


función
Abre_Conexion_Unix(),
Acepta_Conexion_Inet() o
Acepta_Conexion_Cliente(),
• un buffer en el que queramos
recibir (puede ser un puntero a
cualquier estructura de datos
que ya esté creada haciendo el
cast adecuado)
• el número 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 número 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 Lee un mensaje de un socket UDP,
sockaddr *, obtenido con Abre_Conexion_Udp() o
int *, char *, int) Abre_Socket_Udp(). La función se
queda bloqueada hasta que llegue el
mensaje o se produzca algún error. Se
le pasa:

• El descriptor del socket del


que debe leer.
• Una estructura sockaddr.
Cuando se lea el mensaje, en
esta estructura la función nos
devolverá los datos del que
nos ha enviado el mensaje.
Debemos guardarlos si
queremos responderle.
• Un puntero a entero. El entero
debe contenter el tamaño de la
estructura anterior.
• El buffer donde la función
dejará el mensaje. Puede ser
un puntero a cualquier
estructura de datos que
queramos, haciendo el cast a
char *.
• La longitud del mensaje que
queremos leer.

La función devuelve el número de


bytes leidos o -1 en caso de error.
Envía un mensaje por un socket UDP,
obtenido con Abre_Conexion_Udp() o
Abre_Socket_Udp(). Se le pasa:

• El descriptor del socket por el


que va a enviar el mensaje.
• Una estructura sockaddr con el
destinatario del mensaje. Esta
estructura la obtenemos con la
función anterior al recibir un
mensaje, o bien con la función
Dame_Direccion_Udp().
int Escribe_Socket_Udp (int, • Un entero con el tamaño de la
struct sockaddr *,int, char *, estructura anterior.
int) • El buffer con el mensaje que
queremos enviar. Puede ser un
puntero a cualquier estructura
de datos que queramos,
haciendo el cast a char *
• Un entero con el tamaño del
mensaje que queremos enviar
(que está contenido en el
buffer anterior).

La función devuelve el número de


bytes escritos en el socket o -1 en caso
de error.
int Dame_Direccion_Udp (char *, Esta función es útil para rellenar
estructuras sockaddr de una forma
sencila.
Se le pasa:

• el nombre de un host (dado de


alta en /etc/hosts). Puede ser
NULL si la estructura se usa
para un bind() de un servidor
udp.
• el nombre de un servicio (dado
de alta en /etc/services como
udp). Puede ser NULL si
queremos que el sistema
operativo elija un
char *, struct sockaddr *, int servicio/puerto que esté libre
*) (útil para bind() de clientes
udp)
• un puntero a una estructura
sockaddr, que nos devolverá
rellena con el host y servicio
indicado.
• un puntero a entero, en el que
pasamos el tamaño de la
estructura. Este parámetro se
ignora.

La función 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 función close() pasándole el descriptor
del socket. No he hecho una función para eso.

Para obtener la librería, necesitas los ficheros Socket.c, Socket.h, Socket_Servidor.c,


Socket_Servidor.h, Socket_Cliente.c, Socket_Cliente.h y Makefile. Descárgalos,
quítales la extensión .txt y compilalos con make. Se creará un librería libChSocket.a.
Acuérdate del directorio donde dejas todo esto, que en los ejemplos te hará falta indicar
el path de esta librería y de sus .h. También puedes descargarlo todo junto con
ChSocket.tar.gz

Para utilizarla con tus propios programas, si <path> es el directorio donde has puesto
todo esto, la orden de compilación sería parecida a esto

cc miprograma.c -I<path> -L<path> -lChSocket -o miprograma


ejemplo de código con esta segunda opción. Un programa servidor atenderá conexiones
de clientes. A cada uno le asignará un número de cliente y se lo enviará. Tendremos un
programa cliente que cada segundo envía su número de cliente al servidor. Podremos
lanzar hasta 10 clientes simultaneamente y todos serán atendidos. En el ejemplo básico
de sockets se hicieron unos ficheros con funciones útiles. Aquí se han extraido en una
librería que se necesita para el ejemplo.

Para facilitarnos el tratamiento con un solo proceso, tenemos la función select(), Si


tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y
disponemos de sus descriptores, podemos pasarselos a la función select(). Si así lo
deseamos, nuestro código se quedará dormido hasta que en alguno de los descriptores
haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos
envía un mensaje).

Los parámetros de la función select() son los siguientes:

• int con el valor del descriptor más alto que queremos tratar más 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 irán asignando cada vez que abramos algún "fichero". Aquí debemos dar el
valor más alto del descriptor que queramos pasar a la función más uno.
• fd_set * es un puntero a los descriptores de los que nos interesa saber si hay
algún dato disponible para leer o que queremos que se nos avise cuando lo haya.
También se nos avisará cuando haya un nuevo cliente o cuando un cliente cierre
la conexión.
• 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 conexión
e intentamos escribir, se nos enviará una señal SIGPIPE que hará que nuestro
programa se caiga (salvo que tratemos la señal). Para nuestro ejemplo no nos
interesa.
• fd_set * es un puntero a los descriptores de los que nos interesa saber si ha
ocurrido alguna excepción. Para nuestro ejemplo no nos interesa.
• struct timeval * es el tiempo que queremos esperar como máximo. 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
únicamente queremos saber si hay algo en algún descriptor, sin quedarnos
bloqueados.

Cuando la función retorna, nos cambia los contenidos de los fd_set para indicarnos
qué descriptores de fichero tiene algo. Por ello es importante inicializarlos
completamente antes de volver a llamar a la función select().

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

• FD_ZERO (fd_set *) nos vacía el puntero, de forma que estamos indicando que
no nos interesa ningún descriptor de fichero.
• 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().
• 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.
• 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 añadiremos el socket servidor y finalmente, con un bucle, los
sockets clientes. Después llamaremos a la función select(). El código sería más o
menos

fd_set descriptoresLectura;
int socketServidor;
int socketCliente[10];
int numeroClientes;
...
FD_ZERO (&descriptoresLectura);
FD_SET (socketServidor, &descriptoresLectura);
for (i=0; i<numeroClientes; i++)
FD_SET (socketCliente[i], &descriptoresLectura);
...
select (maximo+1, &descriptoresLectura, NULL, NULL, NULL);

Como no tenemos interes en condiciones de escritura ni excepciones, pasamos NULL


en el segundo y tercer parámetro. El último lo ponemos también a NULL puesto que no
tenemos otra tarea que hacer hasta que alguien se conecte o nos envíe 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 conexión. En cualquiera de estas circunstancias,
tenemos que hacer el tratamiento adecuado. La función select() sólo nos avisa de que
algo ha pasado, pero no acepta automáticamente al nuevo cliente, no lee su mensaje ni
cierra su socket.

Por ello, detrás 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 código, después del select(), sería:

/* Se tratan los clientes */


for (i=0; i<numeroClientes; i++)
{
if (FD_ISSET (socketCliente[i], &descriptoresLectura))
{
if ((Lee_Socket (socketCliente[i], (char *)&buffer,
sizeof(int)) > 0))
{
/* Se ha leido un dato del cliente correctamente. Hacer
aquí 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 conexión. Hacer aquí 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, &descriptoresLectura))
{
/* Un nuevo cliente solicita conexión. Aceptarla aquí.
En el ejemplo, se acepta la conexión, se mete el descriptor en
socketCliente[] y se envía al cliente su
posición en el array como número de cliente. */
}

La función Lee_Socket() forma parte de la librería que se comentó anteriormente.


Devuelve lo mismo que la función read(), es decir, el número de bytes leidos, 0 si se
ha cerrado el socket o -1 si ha habido error.

En cuanto al código de ejemplo del cliente, poco tiene que decir. Abre la conexión,
recibe un número de cliente del servidor y se lo reenvia una vez por segundo.

En primer lugar, pera ejecutar el ejemplo, necesitas una mini librería de socket que he
hecho para no tener que repetir el mismo código en todos los ejemplos.

Una vez que tengas la librería, tienes los códigos de ejemplo en servselect.c y
clientselect.c, que se compilan con Makefile. Descárgalos en un directorio distinto al de
la librería (ya que el fichero Makefile, aunque con el mismo nombre, es distinto del de
la librería), quita la extensión .txt. Edita el Makefile del ejemplo y en la línea que
pone

LIBCHSOCKET = ../LIBRERIA

cambia ../LIBRERIA por el path donde hayas descargado y compilado la librería.


Compila el ejemplo con el comando make.

Con permisos de root, en el fichero /etc/services debes añadir una línea que ponga

cpp_java 15557/tcp

El número puede ser el que tú quieras entre 1024 y 65535 siempre y cuando no exista
ya en el fichero. El nombre cpp_java aparece tal cual en el código del ejemplo. Si
quieres puedes poner otro nombre en el /etc/services, pero debes cambiarlo también
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. Verás como
todos son atendidos, hasta un máximo de 10 simultáneamente.
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
aplicación real es demasiado simple. Lo normal es que entre un cliente y un servidor se
intercambien información más compleja, estructuras de datos completas. Vamos a ver
cuales son los mecanismos habituales para esta transmisión de mensajes entre sockets.

LAS ESTRUCTURAS DE DATOS

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

• Pedir la fecha y hora. No necesita enviar datos, símplemente un "algo" que haga
que el servidor le devuelva la fecha hora.
• Pedir el día de la semana. El cliente envía al servidor una fecha y espera que este
le devuelva una cadena de caracteres con el día de la semana.

El servidor, como respuesta, podrá enviar al cliente los mensajes:

• Un long, que es el número de segundos que han transcurrido desde el 1 de Enero


de 1970. Este entero es la forma que tiene unix, por medio de la función time(),
de darnos la fecha/hora del sistema.
• Una cadena de caracteres (pongamos 10 caracteres, para que quepa "Miércoles"
con su fin de cadena '\0' al final). En el código vamos a poner 12 caracteres y
más adelante explicaré por qué.

Lo primero que hay que hacer es escribir en un fichero .h todas estas


estructuras/mensajes. Podemos, si queremos ser más ordenados o si hay muchos
mensajes, escribir dos ficheros .h. En uno irán 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, sólo hay tres estructuras de datos, lo haremos todo junto.

/* No hay estructura de datos para que el cliente pida al servidor la fecha/hora */

/* Estructura de datos para que el cliente pida al servidor el día de la semana */


typedef struct MensajeDameFecha
{
long fechaHora;
} MensajePedirFecha;

/* Estructura de datos que devuelve el servidor al cliente cuando se le pide la fecha/hora


*/
typedef estruct MensajeTomaFecha
{
long fechaHora;
} MensajeTomaFecha;

/* Estructura de datos que devuelve el servidor al cliente cuando se le pide el día de la


semana */
typedef struct MensajeTomaDiaSemana
{
char diaSemana[12];
} MensajeTomaDiaSemana;

Un par de pequeños detalles.

• Cuando se envía un mensaje por un socket, se envía 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
dirección de memoria a la que apunta el puntero (no los datos). Esa dirección de
memoria no tiene ningún sentido en el proceso que recibe el mensaje (en linux
cada proceso tiene su propio espacio de memoria virtual, y más si está en otro
ordenador distinto). Si podemos, sin embargo, poner arrays, como diaSemana,
siempre y cuando ya tengan su tamaño prefijado y fijo.
• Los float y double no son buena elección 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
interpretarán correctamente. Pueden usarse siempre que seamos conscientes de
este problema.
• Veamos el por qué de los 12 caracteres en el día 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 número "mágico". Cuando creamos una estructura,
nuestro compilador se encarga de sus campos queden más o menos alineados
con múltiplos de 4 bytes. Por ejemplo, si ponemos

typedef struct UnaEstructura


{
char caracter;
int numero;
} UnaEstructura;

printf ("El tamaño es %d\n", sizeof(UnaEstructura));

asombrosamente nos saldrá 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 múltiplos 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
estructuras/campos sean múltiplos exactos de 4.

LA CABECERA DEL MENSAJE

El primer problema que encontramos, cuando hay varios mensajes es cómo


identificar qué mensaje vamos a recibir, cuántos bytes tenemos que leer. Por ello, antes
de enviar un mensaje por un socket, se suele enviar una estructura común que hace de
"cabecera" del mensaje. En esa cabecera va la información necesaria para identificar el
mensaje (que se suele llamar "cuerpo") que se va a enviar después. El que recibe el
mensaje, primero lee la cabecera y cuando sabe qué mensaje es el que va detrás, lo lee.

¿Qué contiene una cabecera?.

El único campo imprescindible para la cabecera es un identificador del mensaje que


va detrás. Habitualmente suele ser un entero, de forma que si su valor es 0, el mensaje
que va detrás es uno determinado, si es 1 es otro y así sucesivamente.

typedef struct Cabecera


{
int identificador;
} Cabecera;

Es habitual también 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 único 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 también para los
mensajes que no llevan datos.

typedef Identificadores enum


{
IdDameFecha,
IdDameDiaSemana,
IdTomaFecha,
IdTomaDiaSemana
} Identificadores;

Para enviar el mensaje, por ejemplo, con la función write() se haría lo siguiente

MensajeDameDiaSemana mensaje;
Cabecera cabecera;

mensaje = ...; /* Se rellenan los datos del mensaje */


cabecera.identificador = IdDameDiaSemana; /* Se rellena la cabecera */

/* Se envía primero la cabecera y luego el mensaje */


write (socket, &cabecera, sizeof(cabecera));
write (socket, &mensaje, sizeof(mensaje));

La lectura es algo más compleja. Si leemos con la función read(), nos quedaría algo
así como

Cabecera cabecera;

/* Leemos la cebecera */
read (scket, &cabecera, sizeof(cabecera));
/* En función de la cabecera leida, leemos el mensaje que va a continuación */
switch (cabecera.identificador)
{
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, &mensaje, 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 PEQUEÑA LIBRERÍA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas líneas de código
que deberán realizarse con frecuencia, no es mala idea hacer una función que nos
facilite la tarea y que, de paso, nos "oculte" la estructura exacta de la cabecera. Por
ejemplo, el prototipo de la función podría 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 tamaño de dicha estructura-mensaje. El código de la función, en forma
simple, pordría ser:

void escribeMensaje (int socket, int idMensaje, char *mensaje, int tamanho)
{
/* Se declara y rellena la cabecera */
Cabecera cabecera;
cabecera.identificador = idMensaje;

/* Se envía la cabecera */
write (socket, &cabecera, sizeof(cabecera);

/* Si el mensaje no tiene cuerpo, hemos terminado */


if ((mensaje == NULL) || (tamanho == 0))
return;
/* Se envía el cuerpo */
write (socket, mensaje, tamnho);
}

Una vez hecha la función, su uso es simple:

/* Se declara y rellena el mensaje que queremos enviar */


MensajeDameDiaSemana mensaje;
mensaje = ...;

/* Se envía llamando a nuestra función de librería */


escribeMensaje (socket, IdDameDiaSemana, (char *)&mensaje, sizeof(mensaje));

Para la lectura podemos hacer algo similar. El prototipo de la función sería:

void leeMensaje (int socket, int *idMensaje, char **mensaje);

socket es el socket por el que queremos leer. idMensaje es un puntero a entero. La


función nos lo devolverá relleno con el identificador del mensaje que ha recibido.
mensaje es un puntero a un puntero a char (para ver por qué, mira en punteros como
parámetros). La función creará la memoria necesaria para leer el mensaje y nos la
devolverá como puntero a char. Nosotros, fuera de la función, haremos el cast adecuado
en función del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos.

Si nos ponemos a hacer el código, tendremos algo como esto

void leeMensaje (int socket, int *idMensaje, char **mensaje)


{
Cabecera cabecera;

/* Se lee la cabecera */
read (socket, &cabecera, sizeof(cabecera));

switch (cabecera.identificador)
{
case ... /* creo que hay un problema... */
}
}

Pues parece que tenemos un problema. Si empezamos a hacer los case, nuestra
función va a depender de la mensajería específica de nuestra aplicación. Deberemos
rehacer esta función cada vez que hagamos una aplicación con mensajes distintos o cada
vez que decidamos modificar un mensaje o hacer uno nuevo. Esto no es muy adecuado
para una librería de funciones que queramos que sea más o menos general.

Para resolver este problema, tenemos necesidad de añadir 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 función de escribir no se ve afectada (únicamente hay que rellenar
el nuevo campo con el parámetro tamanho antes de enviar la cabecera). Sin embargo, se
nos facilita enormemente la función de leer. Después de leer la cabecera, hay que crear
un "buffer" del tamaño que indique el campo longitud y leer en ese buffer la estructura,
sin necesidad de saber qué estructura es.

void leeMensaje (int socket, int *idMensaje, char **mensaje)


{
Cabecera cabecera;
*mensaje = NULL; /* Ponemos el mensaje a NULL por defecto */

read (socket, &cabecera, sizeof(cabecera)); /* Se lee la cabecera */

/* Rellenamos el identificador para devolverlo */


*idMensaje = cabecera.identificador;

/* Si hay que leer una estructura detrás */


if (cabecera.longitud > 0)
{
*mensaje = (char *)malloc (longitud); /* Se reserva espacio para leer el mensaje */

read (socket, *mensaje, longtud);


}
}

Con la función hecha así, su utilización sería parecida a esto

char *mensaje = NULL;


int identificador;

/* Se lee el mensaje */
leeMensaje (socket, &identificador, &mensaje);

/* Se hace el tratamiento del mensaje según 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 más
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
aplicación y no en la función de la librería general. Por otra parte es lo lógico, puesto
que la aplicación deberá tratar de distinta manera los mensajes.

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos más que suficiente.
Sin embargo, hay aplicaciones que añaden más campos informativos a la cabecera. No
vamos a entrar en detalle, pero algunos de ellos pueden ser:

• Fecha/Hora de envío del mensaje. Además 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 después que la
fecha/hora contenida en la cabecera. De esta forma quizás se pueda recuperar el
tiempo de retraso. Nuestra función de leer podría fácilmente implementar este
comportamiento.
• Checksum. Un entero que es la suma de todos los bytes del mensaje. Puede o no
incluir la cabecera, según decidamos implementarlo. Se usa como comprobación
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 TCP/IP o UDP/IP 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
comprobación. Si añadimos este campo, estaremos repitiendo algo que ya está
hecho y que SIEMPRE nos va a dar correcto. Este campo tiene más sentido para
otras comunicaciones menos fiables, como un puerto serie sin bit de paridad.
• Número mágico. Cuando leemos, suponemos que nos llega una cabecera y un
mensaje, una cabecera y un mensaje y así sucesivamente. Además, la longitud
del mensaje va contenida en la cabecera. Si al leer o enviar nos equivocamos y
se envía algo incorrecto, es totalemente imposible leer los siguientes mensajes,
ya que nada estará donde esperamos, empezaremos a leer bytes pensando que
son una cabecera cuando en realidad no lo son, etc. A esta situación la
llamaremos "pérdida de sincronismo".
Para detectar esta situación y tratar de corregirla, suele ser habitual hacer que la
cabecera tenga un primer campo entero llamado número mágico 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 número mágico. Tendremos entonces bastantes
posibilidades de que eso sea el principio de una cabecera y podremos recuperar
el sincronismo.

CONCLUSIONES

Con esto queda explicado el cómo enviar mensajes complejos por los sockets e
incluso se abre la posibilidad de incrementar las funciones para nuestra librería de
sockets.

Es más, estas dos funciones son muy generales, puesto que no tienen nada que ver
con sockets. El parámetro socket que se les pasa es en realidad un entero que representa
un descriptor de fichero válido (un socket, un puerto serie o un fichero). Se podrían
poner estas funciones en una librería separada y usarlas para otros tipos de
comunicación o incluso para escribir en fichero estructuras de datos distintas (cada una
con su cabecera). Para leer dicho fichero, se leería cabecera, estructura, cabecera,
estructura y así sucesivamente.

Conceptos

Según vamos haciendo programas de ordenador, nos damos cuenta que algunas
partes del código se utilizan en muchos de ellos. Por ejemplo, podemos tener varios
programas que utilizan números complejos y las funciones de suma, resta, etc son
comunes. También es posible, por ejemplo, que nos guste hacer juegos, y nos damos
cuenta que estamos repitiendo una y otra vez el código para mover una imagen (un
marcianito o a Lara Croft) por la pantalla.

Sería 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:

• No tener que volver a escribir el código (o hacer copy-paste).


• Nos ahorraremos el tiempo de compilar cada vez ese código que ya está
compilado. Además, ya sabemos que mientras hacemos un programa, probamos
y corregimos, hay que compilar entre muchas y "más muchas" veces.
• El código ya compilado estará probado y será fiable. No las primeras veces, pero
sí cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido
corrigiendo los errores.

La forma de hacer esto es hacer librerías. Una librería son una o más 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 función de mover la imagen de Lara Croft, tendremos que hacer la función de
forma que admita cualquier imagen, ya que no nos pegaría nada Lara Croft dando saltos
en un juego estilo "space invaders".

Cómo tenemos que organizar nuestro código

Para poder poner nuestro código en una librería, necesitamos organizarlo de la


siguiente manera:

• Uno o más ficheros fuente .c con el código de nuestras funciones.


• Uno o más 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 serían estos:

libreria1.h libreria1.c
int suma (int a, int b)
#ifndef _LIBRERIA_1_H {
#define _LIBRERIA_1_H return a+b;
}
int suma (int a, int b);
int resta (int a, int b); int resta (int a, int b)
{
#endif 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 librería, no sabemos en qué futuros programas la vamos a utilizar ni cómo
estarán organizados. Supongamos en un futuro programa que hay un fichero de
cabecera fichero1.h que hace #include del nuestro. Imaginemos que hay también un
fichero2.h que también hace #include del nuestro. Finalmente, con un pequeño esfuerzo
más, imaginemos que hay un tercer fichero3.c que hace #include de fichero1.h y
fichero2.h, es decir, más o menos lo siguiente:

fichero1.h fichero2.h fichero3.c


#include <fichero1.h>
#include <libreria1.h> #include <libreria1.h>
#include <fichero2.h>
... ...
...

Cuando compilemos fichero3.c, dependiendo de lo que haya definido en libreria1.h,


obtendremos un error. El problema es que al incluir fichero1.h, se define todo lo que
haya en ese fichero, incluido lo de libreria1.h. Cuando se incluye fichero2.h, se vuelve a
intentar definir lo contenido en libreria1.h, y se obtiene un error de que esas
definiciones están 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 más 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 mayúsculas.

Dentro del bloque #ifndef - #endif, hacemos un #define de ese nombre (no hace falta
darle ningún valor, basta con que esté definido) y luego definimos todos nuestros tipos y
prototipos de funciones.

Cuando incluyamos este fichero por primera vez, _LIBRERIA_1_H no estará


definido, así que se entrará dentro del bloque #ifndef - #endif y se definirán todos los
tipos y prototipos de funciones, incluido el mismo _LIBRERIA_1_H. Cuando lo
incluyamos por segunda vez, _LIBRERIA_1_H ya estará definido (de la inclusión
anterior), por lo que no se entrará en el bloque #ifndef - #endif, y no se redefinirá nada
por segunda vez.

Es buena costumbre hacer esto con todos nuestros .h, independientemente de que
sean o no para librerías. Si te fijas en algún .h del sistema verás que tienes este tipo de
cosas hasta aburrir. Por ejemplo, en /usr/include/stdio.h, lo primero que hay después de
los comentarios, es un #ifndef _STDIO_H.

Librerias estáticas y dinámicas

En linux podemos hacer dos tipos de librerías: estáticas y dinámicas.

Una librería estática es una librería que "se copia" en nuestro programa cuando lo
compilamos. Una vez que tenemos el ejecutable de nuestro programa, la librería no
sirve para nada (es un decir, sirve para otros futuros proyectos). Podríamos borrarla y
nuestro programa seguiría funcionando, ya que tiene copia de todo lo que necesita. Sólo
se copia aquella parte de la librería que se necesite. Por ejemplo, si la librería tiene dos
funciones y nuestro programa sólo llama a una, sólo se copia esa función.

Una librería dinámica NO se copia en nuestro programa al compilarlo. Cuando


tengamos nuestro ejecutable y lo estemos ejecutando, cada vez que el código necesite
algo de la librería, irá a buscarlo a ésta. Si borramos la librería, nuestro programa dará
un error de que no la encuentra.

¿Cuáles son las ventajas e inconvenientes de cada uno de estos tipos de librerías?

• Un programa compilado con librerías estáticas es más grande, ya que se hace


copia de todo lo que necesita.
• Un programa compilado con librerías estáticas se puede llevar a otro ordenador
sin necesidad de llevarse las librerías.
• Un programa compilado con librerías estáticas es, en principio, más rápido en
ejecución. Cuando llama a una función de la librería, la tiene en su código y no
tiene que ir a leer el fichero de la librería dinámica para encontrar la función y
ejecutarla.
• Si cambiamos una librería estática, a los ejecutables no les afecta. Si cambiamos
una dinámica, los ejecutables se ven afectados. Esto es una ventaja si hemos
cambiado la librería para corregir un error (se corrige automáticamente en todos
los ejecutables), pero es un inconveniente si tocar eso nos hace cambiar los
ejecutables (por ejemplo, hemos añadido un parámetro más a una función de la
librería, los ejecutables ya hechos dejan de funcionar).

¿Qué tipo de librería uso entonces?

Es como siempre una cuestión de compromiso entre las ventajas y los inconvenientes.
Para programas no muy grandes y por simplicidad, yo suelo usar librerías estáticas. Las
dinámicas están bien para programas enormes o para librerías del sistema, que como
están en todos los ordenadores con linux, no es necesario andar llevándoselas de un lado
a otro.

En unix las librerías estáticas suelen llamarse libnombre.a y las dinámicas


libnombre.so, donde nombre es el nombre de nuestra librería.

Compilar y enlazar con librerías estáticas

Una vez que tenemos nuestro código, para conseguir una librería estática debemos
realizar los siguientes pasos:

• Obtener los ficheros objeto (.o) de todos nuestros fuentes (.c). Para ello se
compilan con cc -c fuente.c -o fuente.o. La opción -c le dice al compilador que
no cree un ejecutable, sino sólo un fichero objeto. Aquí 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.
• Crear la librería (.a). Para ello se usa el comando ar con los siguientes
parámetros: ar -rv libnombre.a fuente1.o fuente2.o ... La opción -r le dice al
comando ar que tiene que insertar (o reemplazar si ya están dentro) los ficheros
objeto en la librería. La opción -v es "verbose", para que muestre información
mientras está haciendo las cosas. A continuación se ponen todos los fichero
objeto que deseemos. ar es en realidad un comando mucho más genérico que
todo esto y sirve para empaquetar cualquier tipo de fichero (no sólo ficheros
objeto). Tiene además opciones para ver qué 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 estén los fuentes de
la librería y utilizar make para compilarla. Si no sabes de qué estoy hablando, échale un
ojo a la paginilla de los makefiles. Afortunádamente, las reglas implícitas de make ya
saben hacer librerías estáticas. El fichero Makefile quedaría tan sencillo como esto:

Makefile

CFLAGS=-I<path1> -I<path2> ...

libnombre.a: libnombre.a (objeto1.o ojbeto2.o ...)

En CLAGS debes poner tantas opciones -I<path> como directorios con ficheros .h
tengas que le hagan falta a los fuente de la librería para compilar.
La librería depende de los ficheros objetos que hay dentro de ella. Eso se pone
poniendo el nombre de la librería y entre paréntesis los ficheros objeto. Hay algunas
verisones de make que sólo admiten un fichero objeto dentro de los paréntesis. Debe
ponerse entonces

libnombre.a: libnombre.a(objeto1.o) libnombre.a(objeto2.o) ...

Ya tenemos la librería. Ahora, al compilar nuestro programa con el compilador,


debemos decirle dónde están las librerías y cuales son. La orden de compilación
quedaría entonces

$ cc -o miprograma miprograma.c -I<path1> -I<path2> ... -L<path1> -L<path2> ...


-llibreria1 -llibreria2

Los -I<path> son para indicar dónde están los ficheros de cabecera necesarios para la
compilación (tanto propios del programa como los de nuestras librerías).
Los -L<path> son para indicar los directorios en los que se encuentran las librerías.
Los -llibreria son para indicar que se debe coger esa librería. En el comando sólo
ponemos "librería". El prefijo lib y la extensión .a ya la pone automáticamente el
compilador.

Hay un detalle importante a tener en cuenta. Las librerías deben ponerse de forma que
primero esté la de más alto nivel y al final, la de más bajo nivel. Es decir, tal cual lo
tenemos en el ejemplo, libreria1 puede usar funciones de libreria2, pero no al revés. El
motivo es que al compilar se van leyendo las librerías consecutivamente y cargando de
cada una de ellas sólo lo necesario. Vamos a verlo con un ejemplo

Supongamos que miprograma.o llama a la funcion1 de libreria1 y esta funcion1 llama


a funcion2 de libreria2. El compilador lee miprograma.o. 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
función 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 miprograma.c. Como este necesita
funcion1, se apunta como "necesaria". Luego lee libreria2. Como funcion1 no es de esta
libreria y no hay más 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 también que tenemos que tener un cierto orden a la hora de diseñar
librerías. 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 revés.

Existe una pequeña trampa, pero no es muy elegante. Consiste en poner la misma
librería varias veces en varias posiciones. Si en el supuesto que no funcionaba
hubiesemos puesto otra vez al final -llibreria2, habría compilado.

Compilar y "enlazar" con librerías dinámicas

Para compilar los mismos ficheros, pero como librería dinámica, tenemos que seguir
los siguientes pasos:

• Compilar los fuentes, igual que antes, para obtener los objetos.
• Crear la librería con el comando ld. Las opciones para este comando serían ld -o
liblibreria.so objeto1.o objeto2.o .... -shared. La opción -o liblibreria.so le indica
el nombre que queremos dar a la librería. La opción -shared le indica que debe
hacer una librería y no un ejecutable (opción por defecto). objeto1.o,
objeto2.o ... son los ficheros objeto que queremos meter en la librería.

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, ahí
tienes la paginilla de los makes. Desgraciadamente, las reglas implícitas no saben hacer
librerías dinámicas (o, al menos, yo no he visto cómo), así que tenemos que trabajar un
poco más en el Makefile. Quedaría algo así como:

Makefile
liblibreria.so: objeto1.c objeto2.c ...
cc -c -o objeto1.o objeto1.c
cc -c -o objeto2.o objeto2.c
...
ld -o liblibreria.so objeto1.o objeto2.o ... -shared
rm objeto1.o objeto2.o ...
La librería depende de los fuentes. Se compilan para obtener los .o (habría que añadir
además las opciones -I<path> que fueran necesarias), se construye la librería con ld y se
borran los objetos generados. He hecho depender la librería de los fuentes para que se
compile sólo si se cambia un fuente. Si la hago depender de los objetos, como al final
los borro, siempre se recompilaría la librería.

El comando ld es más específico que ar, y no he encontrado opciones para modificar


o borrar los objetos que hay dentro de la librería. No queda más remedio que construir
la librería entera cada vez que se modifique algo.

Una vez generada la librería, para enlazar con ella nuestro programa, hay que poner:

cc -o miprograma miprograma.c -I<path1> -I<path2> ... -L<path1> -L<path2> ...


-Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las librerías estáticas con la excepción del
-Bdynamic. Es bastante habitual generar los dos tipos de librería simultáneamente, con
lo que es bastante normal encontrar de una misma librería su versión estática y su
versión dinámica. Al compilar sin opción -Bdynamic puden pasar varias cosas:

• Existen liblibreria.a y liblibreria.so. Se coge por defecto liblibreria.a


• Sólo existe una de ellas. Se coge la que existe.
• No existe ninguna de ellas. Error.

La opción -Bdynamic cambia el primer caso, haciendo que se coja liblibreria.so en


vez de liblibreria.a. La opción -Bdynamic afecta a todas las librerías que van detrán en
la línea de compilación. Para volver a cambiar, podemos poner -Bstatic en cualquier
momento.

Una vez compilado el ejecutable, nos falta un último paso. Hay que decirle al
programa, mientras se está ejecutando, dónde están las librerías dinámicas, puesto que
las va a ir a buscar cada vez que se llame a una función de ellas. Tenemos que definir la
variable de entorno LD_LIBRARY_PATH, en la que ponemos todos los directorios
donde haya librerías dinámicas de interés.

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:<path1>:<path2>:<path3>
$ export LD_LIBRARY_PATH

Siendo <path> los directorios en los que están las librerías dinámicas. Se ha puesto el
$LD_LIBRARY_PATH pata mantener su valor anterior y añadirle los nuevos
directorios.

¿Te acuerdas del ejemplo del principio con la suma?. Aquí están todos los fuentes
para que puedas jugar con ellos.

• suma.c, resta.c y libreria1.h son los fuentes para la librería. Descárgalos y


quítales la extensión .txt
• principal.c es el fuente para el programa principal que usa las funciones de la
librería. Descárgalo y quítale la extensión .txt
• Makefile es un Makefile para generar todo. Descárgalo y quítale la extensión
.txt. Si haces make p1, se generará la librería estática y se compilara principal.c
con la librería estática para generar un ejecutable p1. Si haces make p2, se
generará la librería dinámica y se compilará principal.c con la librería dinámica
para generar un ejecutable p2.
Si ejecutas ./p2 a pelo no funcionará. Acuérdate de poner el directorio actual (en
el que se supone está la librería dinámica) en la variable de entorno
LD_LIBRARY_PATH.
$ LD_LIBRARY_PATH=.
$ export LD_LIBRARY_PATH
$ ./p2

En el Makefile hay algunas cosillas que he añadido respecto a lo explicado y las


comento. He puesto la opción de compilación -Wall para obtener todos los warning
posibles. También hay un objetivo "clean", que sirve para borrar las librerías y los
ejecutables.

SOCKETS UDP

Aquí suponemos que ya están claros los conceptos de lo que es una arquitectura
cliente/servidor, un socket, un servicio (fichero /etc/services ), etc. Si no es así, puedes
leer el primer ejemplo antes de seguir leyendo en esta página.

Los sockets UDP son sockets no orientados a conexión. Esto quiere decir que un
programa puede abrir un socket y ponerse a escribir mensajes en él 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 conexión, no garantiza que el mensaje llegue a su


destino. Parece claro que si mi programa envía 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 único que garantiza es, que si llega, llega sin errores.

¿Para qué sirve entonces?. Este tipo de sockets se suele usar para información no vital,
por ejemplo, envío de gráficos a una pantalla. Si se pierde algún gráfico por el camino,
veremos que la pantalla pierde un refresco, pero no es importante. El que envía los gráficos
puede estar dedicado a cosas más importantes y enviar los gráficos 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 conexión,
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 cliente/servidor sigue teniendo aquí 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 servicio/puerto que esté libre y envía un mensaje
al servidor solicitando algo. La diferencia principal con los TCP (orientados a conexión), es
que en estos últimos ambos sockets (de cliente y de servidor) están conectados y lo que
escribimos en un lado, sale por el otro. En un UDP los sockets no están conectados, así que
a la hora de enviar un mensaje, hay que indicar quién es el destinatario.

En los sockets orientados a conexión se envían mensajes con write() o send() y se


reciben con read() o recv(). En un socket no orientado a conexión hay que indicar el
destinatario, así que se usan las funciones sendto() y recvfrom().

Veamos todo esto con código y un ejemplo concreto.

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes:

• Abrir un socket con la función socket()


• Asociar el socket a un puerto con bind()
• Leer mensaje con recvfrom().
• Responder mensaje con sendto()

Veamos todo esto con un poco más de detalle.

Abrir el socket

Se abre el socket de la forma habitual, con la función socket(). Esto símplemente nos
devuelve un descriptor de socket, que todavía no funciona ni es útil. La forma de llamarla
sería la siguiente:

int Descriptor;
Descriptor = socket (AF_INET, SOCK_DGRAM, 0);

El primer parámetro indica que es socket es de red, (podría ser AF_UNIX para un
socket entre procesos dentro del mismo ordenador).
El segundo indica que es UDP (SOCK_STREAM indicaría un socket TCP orientado a
conexión).
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 explicó en el ejemplo simple de socket, damos aquí un pequeño repaso al


tema del puerto/servicio y el fichero /etc/services.

En unix para el establecimiento de conexiones con sockets hay 65536 puertos


disponibles, del 0 al 65535. Del 0 al 1023 están reservados para el sistema. El resto están
a nuestra disposición 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 función bind().

Para decir que puerto queremos atender, hay dos opciones:

• La primera es indicar directamente qué número queremos. Esta opción no es buena


puesto que nadie, salvo nuestro programa sabrá que estamos atendiendo ese puerto.
• La segunda es escribir en el fichero /etc/services (con permisos de root) el número
de puerto que queremos atender y darle un nombre que nos recuerde la utilidad de
nuestro servicio. Por ejemplo, un servidor de páginas web suele atender al puerto
80 y es normal encontrar en este fichero la siguiente línea

www 80/tcp 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 queramos.
80 es el número de servicio. Este número debe ser conocido por todos los
navegadores que quieran acceder a este servidor de páginas web.
tcp indica que este puerto es orientado a conexión. Para conectarnos desde un
navegador a este puerto, debemos usar un protocolo orientado a conexión.
http es un segundo nombre que damos al servicio.
Lo que va detrás de # es un comentario en el fichero.

En nuestro ejemplo hemos dado de alta en el fichero /etc/services un servicio al que


hemos llamado cpp_java, que es udp y con un número de puerto. Es decir, hemos añadido en
dicho fichero una línea como esta:

cpp_java 25558/udp # servicio para programa de pruebas.

Vamos ahora con los detalles del código.

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 función
bind(). La forma de llamarla es la siguiente:

struct sockaddr_in Direccion;


... /* Aquí hay que rellenar la estructura Direccion */
bind ( Descriptor, (struct sockaddr *)&Direccion, sizeof (Direccion));

El primer parámetro es el descriptor de socket obtenido con la función socket().

El segundo parámetro 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 máquina AF_UNIX, tenemos la estructura sockaddr_un, que también es
compatible con sockaddr).

El tercer parámetro es el tamaño de la estructura sockaddr_in.


¿ Cómo rellenamos la estructura sockaddr_in ?. Hay tres campos que debemos rellenar:

Direccion.sin_family = AF_INET;
Direccion.sin_port = ... ; /* Este campo se explica cómo rellenarlo un poco más adelante */
Direccion.sin_addr.s_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 dirección IP del cliente al que queremos atender. Poniendo
INADDR_ANY atenderemos a cualquier cliente.
El campo sin_port es el número de puerto/servicio. Para hacerlo bien, debemos leer del
fichero /etc/services el número del servicio cpp_java. Para ello tenemos la función
getservbyname(). La llamada a esta función es de la siguiente manera:

struct servent *Puerto = NULL;


Puerto = getservbyname (Servicio, "udp");

Direccion.sin_port = Puerto->s_port;

Se declara un puntero a la estructura servent. La función getservbyname() recibe el


nombre del servicio y si es "tcp" o "udp". Busca en el fichero /etc/services 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 número del
puerto. Basta asignarlo a Direccion.sin_port.

Con esto el socket del servidor está dispuesto para recibir y enviar mensajes.

Recibir mensajes en el servidor

La función para leer un mensaje por un socket upd es recvfrom(). Esta función admite
seis parámetros (función compleja donde las haya). Vamos a verlos:

• int que es el descriptor del socket que queremos leer. Lo obtuvimos con socket().
• char * que es el buffer donde queremos que nos devuelva el mensaje. Podemos
pasar cualquier estructura o array que tenga el tamaño suficiente en bytes para
contener el mensaje. Debemos pasar un puntero y hacer el cast a char *.
• int que es el número de bytes que queremos leer y que compondrán el mensaje. El
buffer pasado en el campo anterior debe tener al menos tantos bytes como
indiquemos aquí.
• int con opciones de recepción. De momento nos vale un 0.
• struct sockaddr otra vez. Esta vez tenemos suerte y no tenemos que rellenarla. La
pasaremos vacía y recvfrom() nos devolverá 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 parámetro podemos pasar NULL. Ojo, si lo hacemos
así, no tenemos forma de saber quién nos ha enviado el mensaje ni de responderle.
• int * es el puntero a un entero. En él debemos poner el tamaño de la estructura
sockaddr. La función nos lo devolverá con el tamaño de los datos contenidos en
dicha estructura.
Nuestro código para nuestro ejemplo quedaría, para recibir un mensaje,

struct sockaddr_in Cliente; /* Contendrá los datos del que nos envía el mensaje */
int longitudCliente = sizeof(Cliente); /* Tamaño de la estructura anterior */
int buffer; /* Nuestro mensaje es simplemente un entero, 4 bytes. */

recvfrom (Descriptor, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&Cliente,


&longtudCliente);

La función se quedará bloqueada hasta que llegue un mensaje. Nos devolverá el número
de bytes leidos o -1 si ha habido algún error.

Responder con un mensaje al cliente

La función para envío de mensajes es sendto(). Esta función admite seis parámetros, que
son los mismos que la función recvfrom(). Su signifcado cambia un poco, así que vamos a
verlos:

• int con el descriptor del socket por el que queremos enviar el mensaje. Lo
obtuvimos con socket().
• 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.
• int con el tamaño del mensaje anterior, en bytes.
• int con opciones. De momento nos vale poner un 0.
• struct sockaddr. Esta vez sí tiene que estar relleno, pero seguimos teniendo
suerte, nos lo rellenó la función recvfrom(). Poniendo aquí la misma estructura que
nos rellenó la función recvfrom(), estaremos enviando el mensaje al cliente que nos
lo envío a nosotros previamente.
• int con el tamaño de la estructura sockaddr. Vale el mismo entero que nos devolvió
la función recvfrom() como sexto parámetro.

Nuestro código, después de haber recibido el mensaje del cliente, quedaría más o menos

buffer = ...; /* Rellenamos el mensaje de salida con los datos que queramos */

sendto (Descriptor, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&Cliente,


longitudCliente);

La llamada envía el mensaje y devuelve el número de bytes escritos o -1 en caso de error.

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes:

• Abrir un socket con socket()


• Asociar el socket a un puerto con bind()
• Enviar mensaje al servidor con sendto()
• 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 función bind(). Hay pequeñas diferencias
en la forma de rellenar el segundo parámetro (la estructura sockaddr), así que las contamos
aquí.

Direccion.sin_family = AF_INET;
Direccion.sin_port = 0; /* Dejamos que el sistema elija el puerto, uno libre cualquiera */
Direccion.sin_addr.s_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 así porque el cliente no
necesita tener un puerto conocido por el servidor. Normalmente el cliente es el que
comienza la comunicación pidiéndole algo al servidor. Cuando el mensaje de petición llega al
servidor, también llega el puerto y máquina en la que está el cliente, con lo que el servidor
podrá responderle.

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la función es sendto(). Los parámetros y forma de


funcionamiento es igual que en el servidor, pero con la diferencia de que esta vez sí que
tenemos que rellenar la estructura sockaddr del quinto parámetro. La forma de hacerlo es
la siguiente:

Direccion.sin_family = AF_INET;
Direccion.sin_port = ...; /* Aquí debemos dar el puerto del servidor, el cpp_java del
fichero /etc/services */
Direccion.sin_addr.s_addr = ...; /* Aquí debemos dar la dirección IP del servidor, en
formato de red. Lo vemos más abajo */

La forma de rellenar el campo sin_port ya nos la sabemos. El servicio cpp_java debe


estar en el fichero /etc/services de la máquina donde corre el cliente, con el mismo número
de puerto que se puso en la del servidor. Se obtiene el puerto con la función
getservbyname(), igual que se hizo en el bind() del servidor.

La dirección s_addr del servidor se debe obtener con la función gethostbyname(). En el


fichero /etc/hosts de la máquina hay una lista de nombres de máquinas 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 parámetro el
nombre de una máquina que esté dada de alta en el fichero /etc/hosts. En nuestro ejemplo,
puesto que servidor y cliente van a correr en la misma máquina, usaremos como nombre de
máquina "localhost", que está dado de alta por defecto en cualquier instalación de linux. La
llamada se haría así:

struct hostent *Maquina;

Maquina = gethostbyname ("localhost");


Direccion.sin_addr.s_addr = ((struct in_addr *)(Maquina->h_addr))->s_addr;

Bueno, la asignación parece un poco compleja. Veamos de donde sale.

La estructura Maquina tiene un campo h_addr, de ahí la parte Maquina->h_addr.


Este campo no es del tipo deseado, así que se convierte con un cast a struct in_addr, de
ahí lo de (struct in_addr *)(Host->h_addr).
Bueno, pues todo esto es a su vez una estructura, de la que nos interesa el campo
s_addr, así que metemos todo entre paréntesis, cogemos el campo y nos queda lo que
tenemos puesto en el código ((struct in_addr *)(Host->h_addr))->s_addr

Una vez rellena esta compleja estructura, ya podemos enviar el mensaje al servidor,
igual que hicimos en él, 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 rellenará la función 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 Servidor.c y Cliente.c.

En nuestro ejemplo usamos la mini-librería que construimos para el uso de sockets. Si


quieres probar el ejemplo, debes:

1. Bajarte la librería ChSocket.tar.gz en un directorio, descomprimirla y compilarla


desde una shell de linux con
$ make
2. Bajarte los fuentes Makefile, Servidor.c y Cliente.c en otro directorio distinto del
de la librería y quitarles la extensión .txt
3. Cambiar la línea del Makefile que pone
DIRLIBSOCKET=../LIBRERIA
y poner tú el directorio donde te hayas bajado y compilado la librería.
DIRLIBSOCKET=<<MiDirectorioDeLibreria>>
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 enviará un número aleatorio entre 0 y 19 al servidor y este se lo
devolverá 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 más 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 dirección más baja de memoria, mientras que los Sparc, por
ejemplo, lo ponen al revés. Es decir, el 1 en Pentium se representa como cuatro bytes de
valores 01-00-00-00, mientras que en Sparc o la máquina 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 360/370, Motorola, Sparc, HP
PA y la máquina virtual Java). Aquí http://old.algoritmia.net/soporte/endian.htm tienes
un enlace donde se cuenta esto con un poco más de detalle.

Si mandamos un entero a través 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 interpretará incorrectamente.

La máquina 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 carácter a través de un socket, enviamos 2
bytes, mientras que si lo leemos del socket desde un programa en C, sólo leeremos un
byte, dejando el otro "pendiente" de lectura.

De los float y double mejor no hablar. Hay también varios formatos y la conversión de
unos a otros no es tan fácil como dar la vuelta a cuatro bytes. Suele ser buena idea si
queremos comunicar máquinas distintas hacer que no se envíen floats ni doubles.

SOLUCIÓN

La solución a estos problemas pasa por enviar los datos de una forma más 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
código necesario para "darle la vuelta" a los bytes. Cuando lo leemos, debemos también
"darle la vuelta" a lo que hemos leido antes de utilizarlo. Este código sólo valdría para
un Pentium. Si llevamos el código fuente de nuestro programa a un linux que corra en
un microprocesador Sparc, debemos borrar todo este código de "dar vuelta" a los bytes.

Afortunadamente, tanto en C de linux como de windows (con winsocket), tenemos la


familia de funciones htonl().

• htonl() pasa un entero de formato hardware (el del micro) a formato red
(Hardware TO Network).
• ntohl() pasa un entero de formato red a formato hardware.
• htons() hace lo mismo que htonl(), pero con un short (16 bits).
• ntohs() hace lo mismo que ntohl(), pero con un short

Estas funciones están 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 depués de leerlo del socket llamamos a ntohl(), el entero circulará por la red en un
formato estándar y cualquier programa que lo tenga en cuenta será capaz de leerlo.

Llamando a estas funciones nuestro código fuente es además portable de una máquina a
otra. Bastará 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 único de momento que utiliza dos bytes, en
el ejemplo he optado por hacer que sea Java el que convierta esos caracteres a un único
byte antes de enviar y los reconvierta a dos cuando los recibe. La clase String de Java
tiene métodos que permiten hacer esto.

EL CÓDIGO C

Vamos a hacer un cliente y un servidor en C. Cuando se conecten, el cliente enviará un


entero indicando cuántos caracteres van detrás (incluido un caracter nulo al final) y
luego los caracteres. Es decir, enviará, por ejemplo, un 5 y luego "Hola" y un caracter
nulo. Cuando lo reciba, el servidor contestará con un 6 y luego "Adiós" y un caracter
nulo.

Para este código usaremos la mini-librería que hicimos en su momento. También


utilizaremos el servicio cpp_java que dimos de alta en su momento en el /etc/services.
En el ejemplo hemos puesto a este servicio el puerto 25557.

Al establecer la conexión el servidor llamará a la función Abre_Socket_Inet() de la min-


librería. Se pasará a esta función como parámetro en nombre del servicio "cpp_java".
Luego se llamará a la función 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 llamará a la función Abre_Conexion_Inet(), pasándole de


parámetros la máquina 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 pequeño 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 número de puerto/servicio (25557 en
nuestro caso). Si decidimos hacerlo como en el ejemplo, es decir, dar de alta el servicio
en el fichero /etc/services y leerlo con la función getservbyname(), no hay ningún
problema. Sin embargo, si decidimos en nuestro código poner el número "a pelo", hay
que tener en cuenta que dicho número va a circular por red. El cliente enviará al
servidor este número de puerto para indicarle a qué puerto conectarse. Este número, 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
usará la función htons(). El código sería más o menos:

struct sockaddr_in Direccion;


...
Direccion.sin_port = htons (25557);

Todo esto, repito, sería si no utilizamos las funciones de la mini-libreria, que suponen
que el servicio está de alta en /etc/services y que utilizan la función getservbyname().

Tanto en cliente como servidor, ara el envío de datos usaremos la función


Escribe_Socket() que lleva varios parámetros. Antes de enviar un entero, debemos
convertirlo a formato red. Por ejemplo, en el cliente tenemos el siguiente código.

int Longitud_Cadena;
int Aux;
...
Longitud_Cadena = 6;
Aux = htonl (Longitud_Cadena); /* Se mete en Aux el entero en formato red */
/* Se envía Aux, que ya tiene los bytes en el orden de red */
Escribe_Socket (Socket_Con_Servidor, (char *)&Aux, sizeof(Longitud_Cadena));
...
char Cadena[100];
...
strcpy (Cadena, "Adios");
Escribe_Socket (Socket_Con_Servidor, Cadena, Longitud_Cadena);

En cuanto a la lectura, se usará la función Lee_Socket(). A los enteros leidos hay que
transformarlos de formato red a nuestro propio formato con la función ntohl(). El
código para el cliente sería

int Longitud_Cadena;
int Aux;
...
Lee_Socket (Socket_Con_Servidor, (char *)&Aux, sizeof(int)); /* La función 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 más por la parte de C. Como se puede ver, el único truco consiste en llamar a la
función htonl() antes de enviar un entero por el socket y a la función ntohl() después de
leerlo.

Puedes ver el código en Servidor.c y Cliente.c. Para compilarlo tines que quitarles la
extensión .txt, descargarte el Makefile (también 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 CÓDIGO JAVA

El servidor y cliente que haremos en java harán 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 deberían funcionar igual.

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

Los datos a enviar los encapsulamos en una clase DatoSocket. Esta clase contendrá 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
entendería, debemos hacer un par de métodos 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 método public void writeObject(java.io.DataOutputStream
out) que nos permite escribir estos dos atributos por un stream.

public void writeObject(java.io.DataOutputStream out)


throws IOException
{
// Se envía la longitud de la cadena + 1 por el \0 necesario en C
out.writeInt (c+1);

// Se envía la cadena como bytes.


out.writeBytes (d);

// Se envía el \0 del final


out.writeByte ('\0');
}

También hacemos el método public void readObject(java.io.DataInputStream in) que


nos permita leer los atributos de un stream

public void readObject(java.io.DataInputStream in)


throws IOException
{
// Se lee la longitud de la cadena y se le resta 1 para eliminar el \0 que
// nos envía C.
c = in.readInt() - 1;

// Array de bytes auxiliar para la lectura de la cadena.


byte [] aux = null;
aux = new byte[c]; // Se le da el tamaño

in.read(aux, 0, c); // Se leen los bytes


d = new String (aux); // Se convierten a String

// Se lee el caracter nulo del final


in.read(aux,0,1);
}
Estos métodos no son "robustos". Deberíamos hacer varias comprobaciones, del estilo si
nos envian una longitud negativa o cero, no deberíamos leer la cadena, etc, etc.

Tanto en el código del cliente como en el del servidor, cuando queramos enviar estos
datos, constuiremos un DataOutputStream y llamaremos al método 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 (cliente.getOutputStream()); //
Se construye el stream de salida
dato.writeObject (bufferSalida); // Se envia el dato

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


tendremos

DataInputStream bufferEntrada = new DataInputStream (cliente.getInputStream()); // Se


contruye el stream de entrada
DatoSocket aux = new DatoSocket(""); // Para guardar el dato leido del socket
aux.readObject (bufferEntrada); // Se lee del socket.

Nada más en la parte de java. Los fuentes son SocketServidor.java, SocketCliente.java y


DatoSocket.java. Puedes bajartelos, quitarles la extensión .txt y compilarlos desde una
shell de unix con

$ javac SocketServidor.java SocketCliente.java DatoSocket.java

Ahora puedes ejecutar, por ejemplo, el servidor java con el cliente C y debería
funcionar, al igual que el servidor C con el cliente java o cualquier combinación
servidor-cliente que se te ocurra.

You might also like