Anda di halaman 1dari 7

CONEXIN CON SOCKETS ENTRE UN PROGRAMA JAVA Y UNO C

En este ejemplo no vamos a contar lo que es un socket, ni cmo se programa un socket en C de linux ni cmo se programa un socket en Java. Lo que s vamos a hacer es conectar un programa Java con uno C por medio de sockets y vamos a detallar un poco los problemas que nos vamos a encontrar a la hora de la conexin y pasarnos datos. Estos problemas son habituales cuando conectamos con sockets programas que corren en plataformas distintas. Aunque en nuestro ejemplo ambos programas van a correr en el mismo PC en linux, los programas Java corren en su propia mquina virtual, lo que hace que sea como si estuvieran corriendo en otro microprocesador, bastante distinto a un Pentium (o el que tengamos en nuestro linux).

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 ms 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 direccin ms baja de memoria, mientras que los Sparc, por ejemplo, lo ponen al revs. Es decir, el 1 en Pentium se representa como cuatro bytes de valores 01-00-00-00, mientras que en Sparc o la mquina 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 mquina virtual Java). Aqu http://old.algoritmia.net/soporte/endian.htm tienes un enlace donde se cuenta esto con un poco ms de detalle. Si mandamos un entero a travs 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 mquina 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 carcter a travs de un socket, enviamos 2 bytes, mientras

que si lo leemos del socket desde un programa en C, slo leeremos un byte, dejando el otro "pendiente" de lectura. De los float y double mejor no hablar. Hay tambin varios formatos y la conversin de unos a otros no es tan fcil como dar la vuelta a cuatro bytes. Suele ser buena idea si queremos comunicar mquinas distintas hacer que no se enven floats ni doubles.

SOLUCIN La solucin a estos problemas pasa por enviar los datos de una forma ms 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 cdigo necesario para "darle la vuelta" a los bytes. Cuando lo leemos, debemos tambin "darle la vuelta" a lo que hemos leido antes de utilizarlo. Este cdigo slo valdra para un Pentium. Si llevamos el cdigo fuente de nuestro programa a un linux que corra en un microprocesador Sparc, debemos borrar todo este cdigo 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 estn 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 depus de leerlo del socket llamamos a ntohl(), el entero circular por la red en un formato estndar y cualquier programa que lo tenga en cuenta ser capaz de leerlo. Llamando a estas funciones nuestro cdigo fuente es adems portable de una mquina 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 mtodos que permiten hacer esto.

EL CDIGO C Vamos a hacer un cliente y un servidor en C. Cuando se conecten, el cliente enviar un entero indicando cuntos caracteres van detrs (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 "Adis" y un caracter nulo. Para este cdigo usaremos la mini-librera que hicimos en su momento. Tambin 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 conexin el servidor llamar a la funcin Abre_Socket_Inet() de la minlibrera. Se pasar a esta funcin como parmetro en nombre del servicio "cpp_java". Luego se llamar a la funcin 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 funcin Abre_Conexion_Inet(), pasndole de parmetros la mquina 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 pequeo 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 nmero 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 funcin getservbyname(), no hay ningn problema. Sin embargo, si decidimos en nuestro cdigo poner el nmero "a pelo", hay que tener en cuenta que dicho nmero va a circular por red. El cliente enviar al servidor este nmero de puerto para indicarle a qu puerto conectarse. Este nmero, 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 funcin htons(). El cdigo sera ms o menos:

struct sockaddr_in Direccion; ... Direccion.sin_port = htons (25557); Todo esto, repito, sera si no utilizamos las funciones de la mini-libreria, que suponen que el servicio est de alta en /etc/services y que utilizan la funcin getservbyname(). Tanto en cliente como servidor, ara el envo de datos usaremos la funcin Escribe_Socket() que lleva varios parmetros. Antes de enviar un entero, debemos convertirlo a formato red. Por ejemplo, en el cliente tenemos el siguiente cdigo.

int Longitud_Cadena; int Aux; ... Longitud_Cadena = 6; Aux = htonl (Longitud_Cadena); /* Se mete en Aux el entero en formato red */ /* Se enva 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 funcin Lee_Socket(). A los enteros leidos hay que transformarlos de formato red a nuestro propio formato con la funcin ntohl(). El cdigo para el cliente sera

int Longitud_Cadena; int Aux; ... Lee_Socket (Socket_Con_Servidor, (char *)&Aux, sizeof(int)); /* La funcin 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 ms por la parte de C. Como se puede ver, el nico truco consiste en llamar a la funcin htonl() antes de enviar un entero por el socket y a la funcin ntohl() despus de leerlo. Puedes ver el cdigo en Servidor.c y Cliente.c. Para compilarlo tines que quitarles la extensin .txt, descargarte el Makefile (tambin 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 CDIGO JAVA El servidor y cliente que haremos en java harn 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 deberan funcionar igual. En java utilizaremos las clases SocketServer y Socket para hacer el servidor y el cliente. Puedes ver cmo 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 entendera, debemos hacer un par de mtodos 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 mtodo 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 enva la longitud de la cadena + 1 por el \0 necesario en C out.writeInt (c+1);

// Se enva la cadena como bytes. out.writeBytes (d); // Se enva el \0 del final out.writeByte ('\0'); } Tambin hacemos el mtodo 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 enva 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 tamao 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 mtodos no son "robustos". Deberamos hacer varias comprobaciones, del estilo si nos envian una longitud negativa o cero, no deberamos leer la cadena, etc, etc. Tanto en el cdigo del cliente como en el del servidor, cuando queramos enviar estos datos, constuiremos un DataOutputStream y llamaremos al mtodo 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 ms en la parte de java. Los fuentes son SocketServidor.java, SocketCliente.java y DatoSocket.java. Puedes bajartelos, quitarles la extensin .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 debera funcionar, al igual que el servidor C con el cliente java o cualquier combinacin servidor-cliente que se te ocurra. Sed buenos.

Anda mungkin juga menyukai