Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Socket entre C y java

En este ejemplo no vamos a contar lo que es un socket, ni cómo se programa un socket en C de linux ni cómo se programa un socket en Java.

Lo que 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 conexión 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 máquina 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 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: FORMATO ESTÁNDAR DE RED

La solución a estos problemas pasa por enviar los datos de una forma más o menos estándar, 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().

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 estándar). 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.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: