Programacion de Socket en Linux
Programacion de Socket en Linux
Programacion de Socket en 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.
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.
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 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.
LA CONEXIÓN
Para poder realizar la conexión entre ambos programas son necesarias varias cosas:
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.
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
Con C en Unix/Linux, los pasos que debe seguir un programa servidor son los
siguientes:
EL CLIENTE
Los pasos que debe seguir un programa cliente son los siguientes:
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
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.
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.
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.
/* La llamada a la función */
Puerto = getservbyname ("Nombre_Servicio", "tcp");
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 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.
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.
Una vez hecho esto, podemos decir al sistema que empiece a atender las llamadas de los
clientes por medio de la función listen()
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;
La función accept() es otra que lleva un parámetro complejo, pero que no debemos
rellenar. Los parámetros que admite son
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
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.
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);
Una vez que tenemos todos los datos necesarios, abrimos el socket igual que en el
servidor.
if (Descriptor == -1)
{
printf ("Error\n");
}
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
ALGUNAS CONSIDERACIONES
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 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.
BIBLIOGRAFÍA
CLIENTE
COMUNES
int Lee_Socket (int, char *, int) Sirve para leer datos de un socket
abierto, Se le pasa:
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 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
• 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.
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);
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:
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
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.
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.
• 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
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.
Para enviar el mensaje, por ejemplo, con la función write() se haría lo siguiente
MensajeDameDiaSemana mensaje;
Cabecera cabecera;
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.
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);
/* 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.
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.
/* Se lee el mensaje */
leeMensaje (socket, &identificador, &mensaje);
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;
}
}
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.
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:
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.
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".
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;
}
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:
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.
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.
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.
¿Cuáles son las ventajas e inconvenientes de cada uno de estos tipos de librerías?
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.
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
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
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
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.
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.
Una vez generada la librería, para enlazar con ella nuestro programa, hay que poner:
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:
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.
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.
¿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 SERVIDOR
Los pasos que debe seguir un programa servidor son los siguientes:
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.
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.
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:
Direccion.sin_port = Puerto->s_port;
Con esto el socket del servidor está dispuesto para recibir y enviar mensajes.
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. */
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.
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 */
EL CLIENTE
Abrir el socket
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.
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 */
Una vez rellena esta compleja estructura, ya podemos enviar el mensaje al servidor,
igual que hicimos en él, con el mensaje sendto().
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 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.
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.
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.
• 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
int Socket_Con_Servidor;
...
Socket_Con_Servidor = Abre_Conexion_Inet ("localhost", "cpp_java");
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().
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.
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
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.