10. Red y E/S

En J2SE tenemos una gran cantidad de clases en el paquete java.net para permitir establecer distintos tipos de conexiones en red. Sin embargo, el soportar esta gran API no es viable en la configuración CLDC dedicada a dispositivos muy limitados. Por lo tanto en CLDC se sustituye esta API por el marco de conexiones genéricas (GCF, Generic Connection Framework), con el que se pretenden cubrir todas las necesidades de conectividad de estos dispositivos a través de una API sencilla.

10.1. Marco de conexiones genéricas

Los distintos dispositivos móviles pueden utilizar distintos tipos de redes para conectarse. Algunos utilizan redes de conmutación de circuitos, orientadas a conexión, que necesitarán protocolos como TCP. Otros utilizan redes de transmisión de paquetes en las que no se establece una conexión permanente, y con las que deberemos trabajar con protocolos como por ejemplo UDP. Incluso otros dispositivos podrían utilizar otras redes distintas en las que debamos utilizar otro tipo de protocolos.

El marco de conexiones genéricas (GFC) hará que esta red móvil subyacente sea transparente para el usuario, proporcionando a éste protocolos estándar de comunicaciones. La API de GFC se encuentra en el paquete javax.microedition.io. Esta API utilizará un único método que nos servirá para establecer cualquier tipo de conexión que queramos, por esta razón recibe el nombre de marco de conexiones genéricas, lo cuál además lo hace extensible para incorporar nuevos tipos de conexiones. Para crear la conexión utilizaremos el siguiente método:

Connection con = Connector.open(url);

En el que deberemos especificar una URL como parámetro con el siguiente formato:

protocolo:direccion;parámetros

Cambiando el protocolo podremos especificar distintos tipos de conexiones. Por ejemplo, podríamos utilizar las siguientes URLs:

"http://j2ee.ua.es/pdm" Abre una conexión HTTP.
"datagram://192.168.0.4:6666" Abre una conexión por datagramas.
"socket://192.168.0.4:4444" Abre una conexión por sockets.
"comm:0;baudrate=9600" Abre una conexión a través de un puerto de comunicaciones.
"file:/fichero.txt" Abre un fichero.

Cuando especifiquemos uno de estos protocolos, la clase Connector buscará en tiempo de ejecución la clase que implemente dicho tipo de conexión, y si la encuentra nos devolverá un objeto que implemente la interfaz Connection que nos permitirá comunicarnos a través de dicha conexión.

CLDC nos proporciona interfaces para cada tipo genérico de conexión, pero las implementaciones reales de los protocolos pertenecen a los perfiles.

Figura 1. Componentes de GCF

El único protocolo que la especificación de MIDP exige que se implemente es el protocolo HTTP. Este protocolo pertenece a MIDP, y no a CLDC como era el caso de las clases genéricas anteriores. Distintos modelos de dispositivos pueden soportar otro tipo de conexiones, pero si queremos hacer aplicaciones portables deberemos utilizar HTTP.

10.2. Conexión HTTP

La conexión mediante el protocolo HTTP es el único tipo de conexión que sabemos que va a estar soportado por todos los dispositivos MIDP. Este protocolo podrá ser implementado en cada modelo de móvil bien utilizando protocolos IP como TCP/IP o bien protocolos no IP como WAP o i-Mode.

Figura 2. Gateway para protocolos no IP

De esta forma nosotros podremos utilizar directamente HTTP de una forma estándar sin importarnos el tipo de red que el móvil tenga por debajo.

Cuando establezcamos una conexión mediante protocolo HTTP, podemos hacer una conversión cast del objeto Connection devuelto a un subtipo HttpConnection especializado en conexiones HTTP:

HttpConnection con = 
    (HttpConnection)Connector.open("http://j2ee.ua.es/datos.txt");

Este objeto HttpConnection contiene gran cantidad de métodos dedicados a trabajar con el protocolo HTTP, lo cuál facilitará en gran medida el trabajo de los desarrolladores.

HTTP es un protocolo de petición/respuesta. El cliente crea un mensaje de petición y lo envía a una determinada URL. El servidor analizará esta petición y le devolverá una respuesta al cliente. Estos mensajes de petición y respuesta se compondrán de una serie de cabeceras y del bloque de contenido. Cada cabecera tendrá un nombre y un valor. El contenido podrá contener cualquier tipo de información (texto, HTML, imágenes, mensajes codificados en binario, etc). Tendremos una serie de cabeceras estándar con las que podremos intercambiar datos sobre el cliente o el servidor, o bien sobre la información que estamos transmitiendo. También podremos añadir nuestras propias cabeceras para intercambiar datos propios.

Una vez creada la conexión, ésta pasará por tres estados:

La conexión nada más crearse se encuentra en estado de configuración. Pasará automáticamente a estado conectada cuando solicitemos cualquier información sobre la respuesta.

10.2.1. Lectura de la respuesta

Vamos a comenzar viendo cómo leer el contenido de una URL. En este caso no vamos a añadir ninguna información al mensaje de petición, ya que no es necesario. Sólo queremos obtener el contenido del recurso solicitado en la URL.

Imaginemos que queremos leer el fichero en la URL http://j2ee.ua.es/datos.txt. Como primer paso deberemos crear una conexión con dicha URL como hemos visto anteriormente. Una vez tengamos este objeto HttpConnection abriremos un flujo de entrada para leer su contenido de la siguiente forma:

InputStream in = con.openInputStream();

Una vez hecho esto, la conexión pasará a estado conectada, ya que estamos solicitando leer su contenido. Por lo tanto en este momento será cuando envíe el mensaje de petición al servidor, y se quede esperando a recibir la respuesta. Con el flujo de datos obtenido podremos leer el contenido de la misma, al igual que leemos cualquier otro flujo de datos en Java.

Dado que en este momento ya se ha enviado el mensaje de petición, ya no tendrá sentido realizar modificaciones en la petición. Es por esta razón por lo que la creación del mensaje de petición debe hacerse en el estado de configuración.

Una vez hayamos terminado de leer la respuesta, deberemos cerrar el flujo y la conexión:

in.close();
con.close();

Con esto la conexión pasará a estado cerrada, liberando todos los recursos.

10.2.2. Mensaje de petición

En muchos casos podemos necesitar enviar información al servidor, como por ejemplo el login y el password del usuario para autentificarse en la aplicación web. Esta información deberemos incluirla en el mensaje de petición. Existen distintas formas de enviar información en la petición.

Encontramos los diferentes tipos de mensajes de petición soportados por MIDP:

HttpConnection.GET

Los parámetros que se envían al servidor se incluyen en la misma URL. Por ejemplo, podemos mandar un parámetro login en la petición de la siguiente forma:
http://j2ee.ua.es/pdm?login=miguel

HttpConnection.POST Los parámetros que se envían al servidor se incluyen como contenido del mensaje. Tiene la ventaja de que se puede enviar la cantidad de datos que queramos, a diferencia del método GET en el que esta cantidad puede estar limitada. Además los datos no serán visibles en la misma URL, ya que se incluyen como contenido del mensaje.
HttpConnection.HEAD No se solicita el contenido del recurso al servidor, sólo información sobre éste, es decir, las cabeceras HTTP.

Podemos establecer uno de estos tipos utilizando el método setRequestMethod, por ejemplo para utilizar una petición POST haremos lo siguiente:

con.setRequestMethod(HttpConnection.POST);

Además podremos añadir cabeceras a la petición con el siguiente método:

con.setRequestProperty(nombre, valor);

Por ejemplo, podemos mandar las siguiente cabeceras:

c.setRequestProperty("IF-Modified-Since", 
    "22 Sep 2002 08:00:00 GMT");
c.setRequestProperty("User-Agent",
    "Profile/MIDP-1.0 Configuration/CLDC-1.0");
c.setRequestProperty("Content-Language", "es-ES");

Con esto estaremos diciendo al servidor que queremos que nos devuelva una respuesta sólo si ha sido modificada desde la fecha indicada, y además le estamos comunicando datos sobre el cliente. Indicamos mediante estas cabeceras estándar que el cliente es una aplicación MIDP, y que el lenguaje es español de España.

10.2.3. Envío de datos en la petición

Cuando necesitemos enviar datos al servidor mediante HTTP mediante nuestra aplicación Java, podemos simular el envío de datos que realiza un formulario HTML. Podremos simular tanto el comportamiento de un formulario que utilice método GET como uno que utilice método POST.

En el caso del método GET, simplemente utilizaremos una petición de tipo HttpConnection.GET e incluiremos estos datos codificados en la URL. Por ejemplo, si estamos registrando los datos de un usuario (nombre, apellidos y edad) podemos incluir estos parámetros en la URL de la siguiente forma:

HttpConnection con = 
    (HttpConnection)Connector.open("http://www.j2ee.ua.es/aplic" +
      "/registraUsuario?nombre=Pedro&apellidos=Lopez+Garcia&edad=25");

Cada parámetro tiene la forma nombre=valor, pudiendo incluir varios parámetros separados por el carácter '&'. Como en la URL no puede haber espacios, estos caracteres se sustituyen por el carácter '+' como podemos ver en el ejemplo.

En el caso de que queramos simular un formulario con método POST, utilizamos una petición de tipo HttpConnection.POST y deberemos incluir los parámetros que enviemos al servidor como contenido del mensaje. Para ello deberemos indicar que el tipo de contenido de la petición es application/x-www-form-urlencoded, y como contenido codificaremos los parámetros de la misma forma que se utiliza para codificarlos en la URL cuando se hace una petición GET:

nombre=Pedro&apellidos=Lopez+Garcia&edad=25

De esta forma podemos enviar al servidor datos en forma de una serie de parámetros que toman como valor cadenas de texto. Sin embargo, puede que necesitemos intercambiar datos más complejos con el servidor. Por ejemplo, podemos querer serializar objetos Java y enviarlos al servidor, o enviar documentos XML.

Para enviar estos tipos de información podemos utilizar también el bloque de contenido, debiendo especificar en cada caso el tipo MIME del contenido que vamos a añadir. Ejemplos de tipos MIME que podemos utilizar para el bloque de contenido son:

application/x-www-form-urlencoded Se envían los datos codificados de la misma forma en la que son codificados por un formulario HTML con método POST.
text/plain Se envía como contenido texto ASCII.
application/octet-stream Se envía como contenido datos binarios. Dentro de la secuencia de bytes podremos codificar la información como queramos. Por ejemplo, podemos codificar de forma binaria un objeto serializado, utilizando un DataOutputStream.

Para establecer el tipo de contenido la cabecera estándar de HTTP Content-Type. Por ejemplo, si añadimos texto ASCII, podemos establecer esta cabecera de la siguiente forma:

con.setRequestProperty("Content-Type", "text/plain");

Para escribir en el contenido del mensaje de petición deberemos abrir un flujo de salida como se muestra a continuación:

OutputStream out = con.openOutputStream();

Podremos escribir en este flujo de salida igual que lo hacemos en cualquier otro flujo de salida, con lo que de esta forma podremos escribir cualquier contenido en el mensaje de petición.

Al abrir el flujo para escribir en la petición provocaremos que se pase a estado conectado. Por lo tanto deberemos haber establecido el tipo de petición y todas las cabeceras previamente a la apertura de este flujo, cuando todavía estábamos en estado de configuración.

10.2.4. Tipo y cabeceras de la respuesta

En estado conectado, además del contenido del mensaje de la respuesta, podemos obtener el estado de la respuesta y sus cabeceras. Los estados de respuesta se componen de un código y un mensaje y nos permitirán saber si la petición ha podido atenderse correctamente o si por el contrario ha habido algún tipo de error. Por ejemplo, posibles estados son:

HttpConnection.HTTP_OK 200 OK
HttpConnection.HTTP_BAD_REQUEST 400 Bad Request
HttpConnection.HTTP_INTERNAL_ERROR 500 Internal Server Error

Este mensaje de estado encabeza el mensaje de respuesta. Si el servidor nos devuelve un mensaje con código 200 como el siguiente:

HTTP/1.1 200 OK

Es que se ha procesado correctamente la petición y nos devuelve su respuesta. Si ha ocurrido un error, nos mandará el código y mensaje de error correspondiente. Por ejemplo, el error 400 indica que el servidor no ha entendido la petición que hemos hecho, posiblemente porque la hemos escrito incorrectamente. El error 500 nos dice que se trata de un error interno del servidor, no de la petición realizada.

Podemos obtener tanto el código como el mensaje de estado con los siguientes métodos:

int cod = con.getResponseCode(); 
String msg = con.getResponseMessage();

Los códigos de estado podemos encontrarlos como constantes de la clase HttpConnection como hemos visto para los tres códigos anteriores.

También podemos utilizar este objeto para leer las cabeceras que nos ha devuelto la respuesta. Nos ofrece métodos para leer una serie de cabeceras estándar de HTTP como los siguientes:

getLength content-length Longitud del contenido, o -1 si la longitud es desconocida
getType content-type Tipo MIME del contenido devuelto
getEncoding content-encoding Codificación del contenido
getExpiration expires Fecha de expiración del recurso
getDate date Fecha de envío del recurso
getLastModified last-modified Fecha de última modificación del recurso

Puede ser que queramos obtener otras cabeceras, como por ejemplo cabeceras propias no estándar. Para ello tendremos una serie de métodos que obtendrán las cabeceras directamente por su nombre:

String valor = con.getHeaderField(nombre);
int valor = con.getHeaderFieldInt(nombre);
long valor = con.getHeaderFieldDate(nombre);

De esta forma podemos obtener el valor de la cabecera o bien como una cadena, o en los datos que sean de tipo fecha (valor long) o enteros también podremos obtener su valor directamente en estos tipos de datos.

Podremos acceder a las cabeceras también a partir de su índice:

String valor = con.getHeaderField(int indice);
String nombre = con.getHeaderFieldKey(int indice);

Podemos obtener de esta forma tanto el nombre como el valor de la cabecera que ocupa un determinado índice.

Esta respuesta HTTP, además de un estado y una serie de cabeceras, tendrá un bloque que contenido que podremos leer abriendo un flujo de entrada en la conexión como hemos visto anteriormente. Normalmente cuando hacemos una petición a una URL de una aplicación web nos devuelve como contenido un documento HTML. Sin embargo, en el caso de nuestra aplicación MIDP este tipo de contenido no es apropiado. En su lugar podremos utilizar como contenido de la respuesta cualquier otro tipo MIME, que vendrá indicado en la cabecera content-type de la respuesta. Por ejemplo, podremos devolver una respuesta codificada de forma binaria que sea leída y descodificada por nuestra aplicación MIDP.

Tanto los métodos que obtienen un flujo para leer o escribir en la conexión, como estos métodos que acabamos de ver para obtener información sobre la respuesta producirán una transición al estado conectado.

10.3. Acceso a la red a bajo nivel

Como hemos comentado, el único tipo de conexión especificada en MIDP 1.0 es HTTP, la cual es suficiente y adecuada para acceder a aplicaciones corporativas. Sin embargo, con las redes 2.5G y 3G tendremos una mayor capacidad en las conexiones, nos permitirán realizar cualquier tipo de conexión TCP y UDP (no sólo HTTP) y además la comunicación podrá ser más fluida.

Para poder acceder a estas mejoras desde nuestra aplicación Java, surge la necesidad de que en MIDP 2.0 se incorpore soporte para tipos de conexiones a bajo nivel: sockets (TCP) y datagramas (UDP). Estos tipos de conexiones de MIDP 2.0 son optativos, de forma que aunque se encuentran definidos en la especificación de MIDP 2.0, no se obliga a que los fabricantes ni los operadores de telefonía lo soporten. Es decir, que la posibilidad de utilizar estas conexiones dependerá de que la red de telefonía de nuestro operador y el modelo de nuestro móvil las soporte.

Si intentamos utilizar un tipo de conexión no soportada por nuestro sistema, se producirá una excepción de tipo ConnectionNotFoundException.

10.3.1. Sockets

Los sockets nos permiten crear conexiones TCP. En este tipo de conexiones se establece un circuito virtual de forma permanente entre los dispositivos que se comunican. Se nos asegura que los datos enviados han llegado al servidor y que han llegado en el mismo orden en el que los enviamos. El inconveniente que tienen es que el tener un canal de comunicación abierto permanentemente consume una mayor cantidad de recursos.

Para abrir una conexión mediante sockets utilizaremos una URL como la siguiente:

SocketConnection sc = 
    (SocketConnection) Connector.open("socket://host:puerto");

Una vez abierta la conexión, podremos abrir sus correspondientes flujos de entrada y salida para enviar y recibir datos a través de ella:

InputStream in = sc.openInputStream();
OutputStream out = sc.openOutputStream();

Es posible también hacer que nuestro dispositivos actúe como servidor. En este caso utilizaremos una URL como las siguientes para crear el socket servidor:

ServerSocketConnection ssc = 
  (ServerSocketConnection) Connector.open("socket://:puerto");
ServerSocketConnection ssc = 
  (ServerSocketConnection) Connector.open("socket://");

En el primer caso indicamos el puerto en el que queremos que escuche nuestro servidor. En el segundo caso este puerto será asignado automáticamente por el sistema. Para conocer la dirección y el puerto donde escucha nuestro servidor podremos utilizar los siguientes métodos:

int puerto = ssc.getLocalPort();
String host = ssc.getLocalAddress();

Para hacer que el servidor comience a escuchar y aceptar conexiones utilizaremos el siguiente método:

SocketConnection sc = (SocketConnection) ssc.acceptAndOpen();

Obtendremos un objeto SocketConnection con el que podremos comunicarnos con el cliente que acaba de conectarse a nuestro servidor.

Debemos tener en cuenta que normalmente los móviles realizan conexiones puntuales cuando necesitan acceder a la red, y cada vez que se conecta se le asigna una nueva IP de forma dinámica. Esto hace difícil que un móvil pueda comportarse como servidor, ya que no podremos conocer a priori la dirección en la que está atendiendo para poder conectarnos a ella desde un cliente.

10.3.2. Datagramas

Cuando trabajemos con datagramas estaremos utilizando una conexión UDP. En ella no se establece un circuito virtual permanente, sino que cada paquete (datagrama) es enrutado de forma independiente. Esto produce que los paquetes puedan perderse o llegar desordenados al destino. Cuando la pérdida de paquetes o su ordenación no sea críticos, convendrá utilizar este tipo de conexiones, ya que consume menos recursos que los circuitos virtuales.

Para trabajar con datagramas utilizaremos una URL como la siguiente:

DatagramConnection dc = 
   (DatagramConnection) Connector.open("datagram://host:puerto");

En este caso no hemos abierto una conexión, ya que sólo se establecerá una conexión cuando se envíe un datagrama, simplemente hemos creado el objeto que nos permitirá intercambiar estos paquetes. Podemos crear un datagrama que contenga datos codificados en binario de la siguiente forma:

byte[] datos = obtenerDatos();
Datagram dg = dc.newDatagram(datos, datos.length);

Una vez hemos creado el datagrama, podemos enviarlo al destinatario utilizando la conexión:

dc.send(dg);

En el caso del servidor, crearemos la conexión de datagramas de forma similar, pero sin especificar la dirección a la que conectar, ya que dependiendo del cliente deberemos enviar los datagramas a diferentes direcciones.

DatagramConnection dc = 
   (DatagramConnection) Connector.open("datagram://:puerto");

El servidor no conocerá las direcciones de sus clientes hasta que haya recibido algún datagrama de ellos. Para recibir un datagrama crearemos un datagrama vacío indicando su capacidad (en bytes) y lo utilizaremos para recibir en él la información que se nos envía desde el cliente de la siguiente forma:

Datagram dg = dc.newDatagram(longitud);
dc.receive(dg);

Una vez obtenido el datagrama, podremos obtener la dirección desde la cual se nos envía:

 String direccion = dg.getAddress();

Ahora podremos crear un nuevo datagrama con la respuesta indicando la dirección a la que vamos a enviarlo. En este caso en cada datagrama se deberá especificar la dirección a la que se envía:

Datagram dg = dc.newDatagram(datos, datos.length, direccion);

El datagrama será enviado de la misma forma en la que se hacía en el cliente. Posteriormente el cliente podrá recibir este datagrama de la misma forma en que hemos visto que el servidor recibía su primer datagrama. De esta forma podremos establecer una conversación entre cliente y servidor, intercambiando estos datagramas.

10.4. Envío y recepción de mensajes

Podemos utilizar la API adicional WMA para enviar o recibir mensajes cortos (SMS, Short Message Service) a través del teléfono móvil. Esta API extiende GFC, permitiendo establecer conexiones para recibir o enviar mensajes. Cuando queramos enviar mensajes nos comportaremos como clientes en la conexión, mientras que para recibirlos actuaremos como servidor. La URL para establecer una conexión con el sistema de mensajes para ser enviados o recibidos a través de una portadora SMS sobre GSM tendrá el siguiente formato:

sms://telefono:puerto

Las clases de esta API se encuentran en el paquete javax.wireless.messaging. Aquí se definen una serie de interfaces para trabajar con los mensajes y con la conexión.

10.4.1. Envio de mensajes

Si queremos enviar mensajes, deberemos crear una conexión cliente proporcionando en la URL el número del teléfono al que vamos a enviar el mensaje y el puerto al que lo enviaremos de forma opcional:

sms://+34555000000
sms://+34555000000:4444

Si no especificamos el puerto se utilizará el puerto que se use por defecto para los mensajes del usuario en el teléfono móvil. Deberemos abrir una conexión con una de estas URLs utilizando GFC, con lo que nos devolverá una conexión de tipo MessageConnection

MessageConnection mc = 
    (MessageConnection)Connector.open("sms://+34555000000");

Una vez creada la conexión podremos utilizarla para enviar mensajes cortos. Podremos mandar tanto mensajes de texto como binarios. Estos mensajes tienen un tamaño limitado a un máximo de 140 bytes. Si el mensaje es de texto el número de caracteres dependerá de la codificación de éstos. Por ejemplo si los codificamos con 7 bits tendremos una longitud de 160 caracteres, mientras que con una codificación de 8 bits tendremos un juego de caracteres más amplio pero los mensajes estarán limitados a 140 caracteres.

WMA permite encadenar mensajes, de forma que esta longitud podrá ser por lo menos 3 veces mayor. El encadenamiento consiste en que si el mensaje supera la longitud máxima de 140 bytes que puede transportar SMS, entonces se fracciona en varios fragmentos que serán enviados independientemente a través de SMS y serán unidos al llegar a su destino para formar el mensaje completo. Esto tiene el inconveniente de que realmente por la red están circulando varios mensajes, por lo que se nos cobrará por el número de fragmentos que haya enviado.

Podremos crear el mensaje a enviar a partir de la conexión. Los mensajes de texto los crearemos de la siguiente forma:

String texto = "Este es un mensaje corto de texto";
TextMessage msg = mc.newMessage(mc.TEXT_MESSAGE);
msg.setPayloadText(texto);

Para el caso de un mensaje binario, lo crearemos de la siguiente forma:

byte [] datos = codificarDatos();
BinaryMessage msg = mc.newMessage(mc.BINARY_MESSAGE);
msg.setPayloadData(datos);

Antes de enviar el mensaje, podemos ver en cuántos fragmentos deberá ser dividido para poder ser enviado utilizando la red subyacente con el siguiente método:

int num_segmentos = mc.numberOfSegments(msg);

Esto nos devolverá el número de segmentos en los que se fraccionará el mensaje, ó 0 si el mensaje no puede ser enviado utilizando la red subyacente.

Independientemente de si se trata de un mensaje de texto o de un mensaje binario, podremos enviarlo utilizando el siguiente método:

mc.send(msg);

10.4.2. Recepción de mensajes

Para recibir mensajes deberemos crear una conexión de tipo servidor. Para ello en la URL sólo especificaremos el puerto en el que queremos recibir los mensajes:

sms://:4444

Crearemos una conexión utilizando una URL como esta, en la que no se especifique el número de teléfono destino.

MessageConnection mc = 
    (MessageConnection)Connector.open("sms://:4444");

Para recibir un mensaje utilizaremos el método:

Message msg = mc.receive();

Si hemos recibido un mensaje que todavía no hay sido leido este método obtendrá dicho mensaje. Si todavía no se ha recibido ningún mensaje, este método se quedará bloqueado hasta que se reciba un mensaje, momento en el que lo leerá y nos lo devolverá.

Podemos determinar en tiempo de ejecución si se trata de un mensaje de texto o de un mensaje binario. Para ello deberemos comprobar de qué tipo es realmente el objeto devuelto, y según este tipo leer sus datos como texto o como array de bytes:

if(msg instanceof TextMessage) {
String texto = ((TextMessage)msg).getPayloadText();
// Procesar texto
} else if(msg instanceof TextMessage) {
byte [] datos = ((BinaryMessage)msg).getPayloadData();
// Procesar datos
}

Hemos visto que el método receive se queda bloqueado hasta que se reciba un mensaje. No debemos hacer que la aplicación se quede bloqueada esperando un mensaje, ya que éste puede tardar bastante, o incluso no llegar nunca. Podemos solucionar este problema realizando la lectura de los mensajes mediante un hilo en segundo plano. Otra solución es utilizar un listener.

10.4.3. Listener de mensajes

Estos listeners nos servirán para que se nos notifique el momento en el que se recibe un mensaje corto. De esta forma no tendremos que quedarnos bloqueados esperando recibir el mensaje, sino que podemos invocar receive directamente cuando sepamos que se ha recibido el mensaje.

Para crear un listener de este tipo deberemos crear una clase que implemente la interfaz MessageListener:

public MiListener implements MessageListener {
public void notifyIncomingMessage(MessageConnection mc) {
// Se ha recibido un mensaje a través de la conexion mc
}
}

Dentro del método notifyIncomingMessage deberemos introducir el código a ejecutar cuando se reciba un mensaje. No debemos ejecutar la operación receive directamente dentro de este método, ya que es una operación costosa que no debe ser ejecutada dentro de los callbacks que deben devolver el control lo antes posible para no entorpecer el procesamiento de eventos de la aplicación. Deberemos hacer que la recepción del mensaje la realice un hilo independiente.

Para que la recepción de mensajes le sea notificada a nuestro listener deberemos registrarlo como listener de la conexión con:

mc.setMessageListener(new MiListener());

En WTK 2.0 tenemos disponible una consola WMA con la que podremos simular el envío y la recepción de mensajes cortos que se intercambien entre los emuladores, de forma que podremos probar estas aplicaciones sin tener que enviar realmente los mensajes y pagar por ellos.

10.5. Servicios Web

Los servicios web son una tecnología interesante para las aplicaciones MIDP, ya que nos permiten acceder a información de nuestras aplicaciones corporativas de forma estándar sobre protocolo HTTP.

En una aplicación MIDP no nos servirá de nada obtener un documento HTML, ya que no es adecuado para presentarlo en la interfaz del móvil, y será muy complicado extraer de él información porque normalmente estará escrito en lenguaje natural y con un formato adecuado para su presentación, pero no para ser entendido por una máquina.

Los servicios web nos permiten obtener únicamente la información que necesitamos, pero no la presentación. Esta información vendrá codificada en XML de forma estándar. Podemos ver los servicios web como una web para aplicaciones, frente a los documentos HTML que serían una web para humanos.

El problema que encontramos con los servicios web es que el procesamiento de XML es bastante costoso en términos de procesamiento y memoria, lo cual lo hace poco adecuado para dispositivos muy limitados. Además la información codificada en XML ocupa mucho más espacio que utilizando la codificación binaria, por lo que tendremos que transferir una mayor cantidad de información a través de la red. Cuando la red es lenta y cara, esto es un gran inconveniente, que hace que los servicios web por el momento no se puedan utilizar en la práctica para este tipo de dispositivos.

Sin embargo, ya existen APIs que nos permiten utilizar esta tecnología desde móviles. Web Services API (WSA) nos permite crear clientes de servicios web SOAP desde dispositivos móviles. Podremos acceder a servicios existentes proporcionados por terceros, o crear nuestros propios servicios para acceder a nuestra aplicación.

Si queremos acceder a servicios proporcionados por terceros deberemos tener en cuenta que WSA sólo soporta servicios de tipo document/literal. Si el servicio al que queremos acceder no es de este tipo (podemos consultar esta información en su documento WSDL) como solución podremos crearnos un servicio propio compatible con WSA que encapsule una llamada al servicio proporcionado por terceros.

10.5.1. Creación del servicio

Vamos a ver como crear un servicio compatible con WSA. Para esto deberemos especificar que debe ser de tipo document/literal. Consideraremos el caso de la creación del servicio con JWSDP 1.3.

Lo primero que deberemos hacer es implementar el servicio, creando una interfaz remota con los métodos que nos ofrece el servicio y una clase Java que implemente esta interfaz donde se definirá la funcionalidad del servicio.

Por ejemplo, podemos crear un servicio con la siguiente interfaz:

package es.ua.j2ee.sw.hola;

import java.rmi.Remote; import java.rmi.RemoteException;

public interface HolaMundoIF extends Remote { public String saluda(String nombre) throws RemoteException; }

Y con la siguiente implementación de la anterior interfaz:

package es.ua.j2ee.sw.hola;

import java.rmi.RemoteException;

public class HolaMundoImpl implements HolaMundoIF {
public String saluda(String nombre) throws RemoteException { return "Hola " + nombre; }
}

Para que el servicio creado sea del tipo document/literal antes de generar el servicio, deberemos generar un modelo donde se especifique el tipo de servicio. Podemos generar el modelo con la herramienta wscompile. Para utilizar esta herramienta necesitamos crear un fichero de configuración de nuestro servicio (config.xml) como el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<configuration 
  xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
<service 
  name="HolaMundoMovil" 
  targetNamespace="http://j2ee.ua.es/sw" 
  typeNamespace="http://j2ee.ua.es/sw" 
  packageName="es.ua.j2ee.sw.hola">
  <interface name="es.ua.j2ee.sw.hola.HolaMundoIF"/>
</service>
</configuration> 

Podemos ejecutar la herramienta wscompile desde línea de comando o desde ant. Para utilizarla desde ant deberemos declarar en el fichero build.xml esta tarea y el classpath necesario para ejecutarla:

<!-- Propiedades -->

<property name="jwsdp.home" value="c:\\jwsdp-1.3"/> <!-- Classpath -->

<path id="compile.classpath"> <fileset dir="${jwsdp.home}/jwsdp-shared/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxp/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxp/lib/endorsed"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxrpc/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/saaj/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/apache-ant/lib"> <include name="*.jar"/> </fileset> </path>

<!-- Definicion de tareas -->

<taskdef name="wscompile" classname="com.sun.xml.rpc.tools.ant.Wscompile"> <classpath refid="compile.classpath"/> </taskdef> <taskdef name="wsdeploy" classname="com.sun.xml.rpc.tools.ant.Wsdeploy"> <classpath refid="compile.classpath"/> </taskdef>

En la llamada a la tarea wscompile deberemos especificar como parámetro que el tipo de servicio es documentliteral y el fichero donde queremos que se genere el modelo:

<target name="generate">
  <wscompile 
     keep="true"
     define="true"
     features="documentliteral"
     base="."
     xPrintStackTrace="true"
     verbose="true"
     model="model.gz"
     config="config.xml">
     <classpath>
       <path refid="compile.classpath"/>
     </classpath>
  </wscompile>
</target>

Una vez tenemos el modelo generado, podemos crear un fichero descriptor de servicios web (jaxrpc-ri.xml) que utilice este modelo:

<?xml version="1.0" encoding="UTF-8"?>
<webServices
   xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/dd"
   version="1.0"
   targetNamespaceBase="http://j2ee.ua.es/wsdl"
   typeNamespaceBase="http://j2ee.ua.es/types"
   urlPatternBase="/ws">
<endpoint name="HolaMundo" displayName="Servicio HolaMundo" description="Servicio Web Hola Mundo" interface="es.ua.j2ee.sw.hola.HolaMundoIF" model="/WEB-INF/model.gz" implementation="es.ua.j2ee.sw.hola.HolaMundoImpl"/>
<endpointMapping endpointName="HolaMundo" urlPattern="/hola"/>
</webServices>

Una vez tenemos implementado el servicio, creado el modelo y el fichero descriptor de servicios web, organizaremos estos ficheros utilizando la estructura de directorios que deben seguir los servicios web de JWSDP:

/WEB-INF/web.xml
/WEB-INF/jaxrpc-ri.xml
/WEB-INF/model.gz
/WEB-INF/classes/es/ua/j2ee/sw/hola/HolaMundoIF.class
/WEB-INF/classes/es/ua/j2ee/sw/hola/HolaMundoImpl.class

Empaquetaremos toda esta estructura en un fichero WAR, y entonces podremos utilizar la tarea wsdeploy para generar el servicio a partir de dicho fichero, de la misma forma que se hace con cualquier otro servicio web en JWSDP. La única novedad en este caso ha sido que hemos incluido en el servicio un fichero model.gz donde se especifica el tipo de servicio que queremos generar.

Si observamos el fichero WSDL generado para el servicio, podremos comprobar que en el apartado binding se especifica que es de tipo document y literal:

<binding name="HolaMundoIFBinding" type="tns:HolaMundoIF">
  <operation name="saluda">
    <input>
      <soap:body use="literal"/>
    </input>
    <output>
      <soap:body use="literal"/>
    </output>
    <soap:operation soapAction=""/>
</operation> <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document"/>
</binding>

Cualquier servicio que sea de este tipo se podrá ejecutar desde clientes J2ME. De esta forma, para comprobar si un servicio es compatible con la implementación de J2ME, podremos consultar su fichero WSDL y ver el tipo especificado en el apartado binding.

10.5.2. Creación del stub

Para acceder al servicio desde nuestra aplicación deberemos crear una capa stub. Esta capa será la encargada de acceder al servicio, de forma que el desarrollador no tenga que preocuparse de implementar este acceso. Simplemente accederemos al stub para invocar los métodos del servicio como si se tratase de un objeto local, sin tenernos que preocupar del mecanismo de invocación subyacente.

WTK 2.1 incluye herramientas para la generación automática de este stub. Podremos generar un stub en nuestro proyecto utilizando la opción Project > Stub Generator ... :

Cuando pulsemos sobre dicha opción, se abrirá una ventana donde deberemos introducir los datos del servicio para el cual queremos generar el stub:

En esta ventana indicaremos la dirección donde se encuentra el documento WSDL del servicio al que vamos a acceder, y el paquete donde generaremos las clases del stub. Una vez hayamos introducido los datos pulsaremos OK y el stub se generará automáticamente.

10.5.3. Invocación del servicio

Una vez tenemos generado el stub para acceder al servicio, podemos utilizar este stub desde nuestra aplicación MIDP. El stub generado será una clase con el nombre de la interfaz de nuestro servicio añadiendo el sufijo _Stub.

Esta clase implementará la misma interfaz que nuestro servicio, de forma que podremos instanciarla e invocar los métodos de este objeto para acceder a las operaciones del servicio como si se tratase de un acceso a un objeto local.

Por ejemplo, en el caso de nuestro servicio web "Hola Mundo", podemos acceder a él desde nuestra aplicación cliente de la siguiente forma:

HolaMundoIF hola = new HolaMundoIF_Stub();
try {
  String saludo = hola.saluda("Miguel");
} catch(RemoteException re) {
  // Error
}

10.6. Conexiones bluetooth

Bluetooth es una tecnología que nos permite conectar dispositivos próximos por radio, sustituyendo de esta forma a las conexiones por cable o infrarrojos. Además bluetooth nos ofrece distintos servicios como voz, fax, modem, conexión por puerto serie para transmisión de datos, etc.

No se debe ver bluetooth como un competidor de las redes inalámbricas 802.11b, ya que cada tecnología tiene un fin distinto. Bluetooth se utilizará para conectar pequeños dispositivos en un radio pequeño (unos 10 metros), mientras que las redes 802.11b se utilizarán para conectar dispositivos más potentes como ordenadores de sobremesa y portátiles en un área más grande. Nos podemos referir a las redes 802.11b como redes de área local (LAN), y a las redes bluetooth como redes de área personal (PAN).

Aunque el alcance normal de bluetooth son 10 metros, aumentando la potencia del punto de acceso bluetooth podemos aumentar su radio de alcance hasta 100 metros.

La tecnología bluetooth se puede utilizar para conectar dispositivos, como por ejemplo kits de manos libres, para intercambiar datos con otros dispositivos, para acceder a funcionalidades u obtener información de dispositivos de nuestro entorno, etc. Podemos crear una red de dispositivos que se conecten entre si utilizando esta tecnología.

10.6.1. Topología de las redes bluetooth

Las redes que se crean utilizando bluetooth son redes "ad hoc", es decir, se crean dinámicamente. La comunicación entre distintos dispositivos hace que se cree un red de forma espontánea.

Los dispositivos bluetooth tienen la capacidad de "descubrir" otros dispotivos bluetooth de su entorno. De esta forma, los dispositivos pueden localizarse entre ellos y conectarse formando una red de forma dinámica, sin tener que haber creado previamente ninguna infraestructura para dicha red. Cada dispositivo tendrá un identificador bluetooth único con el que se identificará.

Las redes bluetooth se forman en grupos llamados piconets. Cada piconet es un grupo que puede contener hasta 8 dispositivos como máximo, en el que uno de ellos será el maestro, y los demás serán esclavos que estarán conectados a este maestro.

Figura 3. Topología de una red bluetooth

Un mismo dispositivo puede pertenecer a dos piconets distintas. Es más, puede que el dispositivo tenga una función distinta en cada piconet, por ejemplo puede actuar como maestro en uno de ellos, y como esclavo en el otro.

Cuando un dispositivo pertenece a varias piconets, estas piconets estarán conectadas entre ellas. Cuanto tenemos varias piconets conectadas de esta forma, todas ellas formarán lo que se conoce como una scatternet. En el caso de las scatternet, dado que existen dispositivos que están conectados al mismo tiempo a dos piconets, el ancho de banda de la red será menor. Además, hay muchos dispositivos que no soportan este tipo de redes.

10.6.2. Capas de protocolos

Vamos a ver los protocolos que se utilizan en la comunicación mediante bluetooth.

Figura 4. Capas de bluetooth

Encontramos las siguientes capas:

Para las comunicaciones bluetooth utiliza multiplexado en el tiempo, para poder utilizar una comunicación full-duplex. Los datos se envían en ranuras de tiempo de 625ms cada una, el maestro utilizará las ranuras impares y los esclavos utilizarán las pares. Un paquete podrá enviarse en un máximo de 5 ranuras de tiempo (2745 bits de longitud).

Las capas por encima de L2CAP serán los distintos protocolos de comunicaciones que podremos utilizar en las aplicaciones que establezcan conexiones bluetooth. Los protocolos que podremos utilizar para comunicarnos mediante bluetooth son:

Cuando dos dispositivos se conectan por primera vez, por motivos de seguridad deben establecer un secreto compartido. Esto es lo que se conoce como pairing. Es decir, los usuarios de los distintos dispositivos que vayan a conectarse deben ponerse de acuerdo e introducir en ellos el mismo código, para de esta forma evitar que se realicen conexiones no deseadas a nuestro dispositivo. Una vez se ha realizado el pairing, este código se guarda en el dispositivo y ya no hará falta introducirlo para las sucesivas conexiones.

Una vez realizado el pairing de los dispositivos, estos pueden conectarse para formar una red bluetooth. Esta red se formará de la siguiente forma:

En las redes bluetooth el dispositivo maestro es el que establece los tiempos y el acceso en la piconet. Además hemos visto que tiene la responsabilidad de añadir a los esclavos a la piconet.

Podemos ver el maestro como el servidor de la red, que gestiona las conexiones con varios clientes que en este caso serían los esclavos. En las conexiones cliente/servidor normalmente el servidor permanece a la escucha esperando peticiones de conexión de los clientes. Sin embargo, en el caso de bluetooth ocurre al contrario, es el servidor (maestro) el que se encarga de solicitar a los clientes (esclavos) que se añadan a la piconet. Para ello estos esclavos deben haber ofrecido (publicado) el servicio necesario, para que el maestro pueda descubrirlo y conectarse a él.

En el caso de conexiones punto-a-punto, es indiferente quien se conecte como esclavo y quien como maestro. Esta decisión tendrá mayor relevancia en el caso de conexiones punto-a-multipunto, en las que todos los esclavos estarán conectados a un mismo maestro.

Vamos a ver ahora cómo establecer estas conexiones bluetooth utilizando las APIs de Java para Bluetooth (JSR-82). Esta API es el primer estándar no propietario que ha aparecido para desarrollar aplicaciones bluetooth utilizando este lenguaje. Podemos distinguir dos APIs independientes:

javax.bluetooth
javax.obex

Esto es así porque OBEX puede funcionar sobre distintos tipos de conexiones como cable o infrarrojos. De esta forma la API OBEX no estará ligada a la de bluetooth, sino que será independiente, pudiendo así ser utilizada para trabajar con estro protocolo sobre los demás tipos de conexiones.

La API de bluetooth soporta los protocolos L2CAP, SDP y RFCOMM, pero no soporta comunicaciones de voz. Con la API OBEX también podremos utilizar este protocolo.

A partir de WTK 2.2 se incluye soporte para bluetooth en este kit de desarrollo. Para poder probar las aplicaciones bluetooth podemos usar esta versión que, además de incorporar la API JSR-82, incluye emuladores que simulan este tipo de conexiones. Podremos ejecutar varias instancias del emulador y simular conexiones bluetooth entre ellos, sin necesitar disponer de dispositivos bluetooth reales.

10.6.3. Registrar servicios

Lo primero que deberemos hacer para crear una red bluetooth es registrar los servicios de los esclavos, para que el maestro sea capaz de localizarlos y establecer una comunicación con ellos.

Deberemos asignar un UUID al servicio que vayamos a crear. Deberemos asegurarnos de que el UUID que generemos sea un identificador único que identifique el tipo de servicio que estamos implementando.

Un UUID es un número de 128 bits que tiene la siguiente forma (en hexadecimal):

UUID = 00000000-0000-1000-8000-0014e3a325f9
       32       16   16   16   48 
bits bits bits bits bits

Podemos distinguir varios bloques dentro de este número con distinto número de bits cada uno de ellos.

Para generar este UUID podemos utilizar herramientas como uuidgen, que suelen generar este número basándose en el instante de tiempo actual o en generación de números aleatorios.

También podemos generar este número manualmente. Para garantizar que sea único, podemos poner como el último bloque de 48 bits el identificador de nuestro dispositivo bluetooth, y en el resto de bloques podremos poner lo que queramos, siempre que para dos servicios que hagamos no utilicemos el mismo UUID.

Una vez hayamos generado un UUID para nuestro servicio, podemos introducirlo como constante en el código de nuestra aplicación para tener acceso a él cuando sea necesario, tanto desde el cliente como desde el servidor.

public final static String UUID = "000000000000010008000123456789ab";

Para poder registrar nuestro servicio, lo primero que debemos hacer es establecer nuestro dispositivo como descubrible. Para ello accederemos a nuestro dispositivo local a través de un objeto LocalDevice que obtenemos de la siguiente forma:

LocalDevice ld = LocalDevice.getLocalDevice();

Con este objeto LocalDevice podremos obtener datos de nuestro dispositivo local como su dirección bluetooth o su nombre.

String bt_addr = ld.getBluetoothAddress();
String bt_name = ld.getFriendlyName();

Una vez tenemos acceso a este objeto, podremos cambiar el modo de ser descubierto del mismo utilizando el método setDiscoverable. Hay tres modos de ser descubiertos:

Si no consiguiésemos hacer nuestro dispositivo localizable, lanzaremos una excepción para interrumpir el flujo del programa, ya que no podremos publicar servicios si nadie va a poder descubrirlos.

if (!ld.setDiscoverable(DiscoveryAgent.GIAC)) {
  // Lanzar excepcion, no se puede descubrir
}

Una vez establecido el dispositivo local como descubrible, pasaremos a crear y registrar el servicio. Deberemos definir una URL para nuestro servicio a partir de su UUID. En la URL especificaremos el protocolo de comunicaciones que vamos a utilizar. Para utilizar RFCOMM utilizaremos el protocolo btspp (Bluetooth Serial Port Protocol), mientras que para L2CAP utilizaremos btl2cap:

String url = "btspp://localhost:" + UUID;

Utilizando esta URL crearemos una conexión que será la encargada de atender para prestar dicho servicio:

StreamConnectionNotifier scn = 
(StreamConnectionNotifier)Connector.open(url);

Con esto tendremos ya nuestro servicio registrado en el dispositivo. Podemos obtener un registro del servicio que será accesible tanto desde el lado del cliente como desde el servidor. En este objeto podremos añadir atributos que el cliente del servicio podrá leer.

ServiceRecord sr = ld.getRecord(scn);

Ahora que tenemos el servicio creado y registrado, deberemos atender las conexiones que se produzcan a dicho servicio. Con acceptAndOpen nos quedaremos bloqueados esperando que se conecte algún cliente a nuestro servicio. Una vez conectado obtendremos una conexión de tipo StreamConnection, a partir de la cual podremos abrir flujos de entrada y salida para comunicarnos con el dispositivo remoto según el protocolo que hayamos establecido.

while(true) {
  StreamConnection sc = (StreamConnection)scn.acceptAndOpen();

// Se ha conectado un maestro InputStream is = sc.openInputStream(); OutputStream os = sc.openOutputStream(); // Enviar y recibir datos según el protocolo que establezcamos os.flush(); is.close(); os.close(); sc.close(); }

10.6.4. Descubrimiento de dispositivos

Para establecer una conexión con otro dispositivo, lo primero que necesitaremos es localizar (descubrir) dicho dispositivo.

Para localizar los dispositivos necesitaremos utilizar un objeto DiscoveryAgent que obtendremos a partir del objeto LocalDevice:

LocalDevice ld = LocalDevice.getLocalDevice();
DiscoveryAgent da = ld.getDiscoveryAgent();

La búsqueda se podrá hacer de tres formas diferentes:

RemoteDevice[] dispositivos = 
da.retrieveDevices(DiscoveryAgent.CACHED);
RemoteDevice[] dispositivos = 
da.retrieveDevices(DiscoveryAgent.PREKNOWN);

Vamos a ver cómo descubrir los dispositivos que hay actualmente en nuestro entorno con startInquiry. Necesitaremos definir un listener que herede de DiscoveryListener e implemente los siguientes métodos:

public void deviceDiscovered(RemoteDevice rd, DeviceClass dc);
public void inquiryCompleted(int tipo);
public void servicesDiscovered(int transID, ServiceRecord[] servicios);
public void serviceSearchCompleted(int transID, int estado);

Los dos primeros métodos se utilizarán durante el descubrimiento de dispositivos, mientras que los dos últimos se utilizarán para el descubrimiento de servicios de un dispositivo, como veremos más adelante.

El método deviceDiscovered se invocará cada vez que un dispositivo remoto sea descubierto. El código que introduciremos en él normalmente será para añadir dicho dispositivo a una lista de dispositivos descubiertos:

public void deviceDiscovered(RemoteDevice rd, DeviceClass dc) {
  remoteDevices.addElement(rd);
}

A partir del objeto RemoteDevice obtenido podremos obtener información sobre el dispositivo remoto, como su nombre o su dirección bluetooth, al igual que con LocalDevice obteníamos información sobre el dispositivo local.

Una vez haya terminado la búsqueda de dispositivos se invocará el método inquiryCompleted. En este método podremos introducir código para que, en el caso de haberse completado la búsqueda con éxito, nuestra aplicación pase a realizar la siguiente tarea pendiente, que normalmente será la búsqueda de servicios que nos ofrecen los dispositivos descubiertos.

Cuando hayamos creado el listener, podremos comenzar la búsqueda con startInquiry especificando el modo de búsqueda (GIAC o LIAC) y el listener al que se notificarán los dispositivos encontrados:

da.startInquiry(DiscoveryAgent.GIAC, miListener);

Una vez terminada la búsqueda de dispositivos, necesitaremos buscar los servicios que nos ofrecen, para comprobar si está disponible el servicio que buscamos (el que tiene nuestro UUID).

Para cada dispositivo, podremos buscar los servicios identificados mediante una determinada UUID de la siguiente forma:

da.searchServices(null, new UUID[]{new UUID(UUID,false)},
(RemoteDevice)remoteDevices.elementAt(i),miListener);

Con esto comenzará la búsqueda de servicios del dispositivo especificado. Cada vez que se encuentre uno o varios servicios nuevos, será invocado el método servicesDiscovered con una lista de servicios descubiertos. Para cada servicio remoto encontrado tendremos un objeto ServiceRecord. Podemos añadir los servicios encontrados en una lista, igual que en el caso de los dispositivos:

public void servicesDiscovered(int transID, ServiceRecord[] servicios) {
  for(int i=0;i<servicios.length;i++) {
    remoteServices.addElement(servicios[i]);
  }
}

Cuando haya finalizado la búsqueda de servicios, se invocará el método serviceSearchComplete.

10.6.5. Establecer conexiones

Como último paso, tendremos que establecer una conexión con alguno de los servicios que hayamos localizado. Para ello utilizaremos el objeto ServiceRecord obtenido correspondiente a dicho servicio.

 ServiceRecord rs = (ServiceRecord)remoteServices.elementAt(0);

A partir de este objeto podremos obtener la URL necesaria para conectarnos al servicio:

String url = rs.getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, true);

Con esta URL podremos crear una conexión de tipo flujo y abrir los correspondiente flujos de entrada salida para intercambiar datos con el servidor:

StreamConnection sc = (StreamConnection)Connector.open(url);

InputStream is = sc.openInputStream(); OutputStream os = sc.openOutputStream();
// Enviar y recibir datos según el protocolo que establezcamos os.flush();
os.close();
is.close(); sc.close();

10.6.6. Protocolo L2CAP

En el caso de utilizar directamente el protocolo de bajo nivel L2CAP, utilizaremos una URL como la siguiente para crear la conexión:

String url = "btl2cap://localhost:" + UUID;

Cuando abramos la conexión con Connector.open obtendremos un objeto de tipo L2CAPConnectionNotifier, y mediante el método acceptAndOpen de este objeto podremos aceptar conexiones L2CAP de clientes. Cuando aceptemos una conexión de un cliente obtendremos un objeto L2CAPConnection con el que podremos realizar la comunicación.

Este objeto L2CAPConnection tiene dos métodos send y receive con los que podremos enviar y recibir respectivamente paquetes de datos L2CAP.

Como este protocolo no proporciona control de flujo, este control lo deberemos hacer nosotros si queremos asegurarnos de que los paquetes enviados no se han perdido.

Utilizaremos este protocolo cuando necesitemos una comunicación rápida o cuando la pérdida de paquetes no sea crítica para nuestra aplicación.