Trabajando con SocketChannels en J2SDK 1.4

En este artículo se observa la utilización de SocketChannel y ServerSocketChannel, existentes en Java 2 Standard Development Kit versión 1.4, para crear una aplicación cliente-servidor que puede servir como alternativa a los Web Services, utilizando conexiones HTTP estándares.

La proxima vez que afrontemos una tarea de networking que nos tiene considerando utilizar la Web Services API o alguna otra API de alto nivel, deberíamos considerar si podríamos obtener la misma funcionalidad de un modo más sencillo, y con menos procesamiento al utilizar Sockets. En este artículo utilizaremos las clases SocketChannel y ServerSocketChannel classes en el paquete java.nio para crear aplicaciones simples con capacidad de trabajo en red.

Cuando usamos un browser web para acceder a una página web particular, los detalles se mantienen ocultos de nuestros ojos. Abramos un browser y tipeemos:

http://developer.java.sun.com/developer/JDCTechTips/

Como resultado, esta entrada nos conectará al puerto 80 del sitio web. También envía un pedido HTTP (HTTP request) particular que contiene el nombre de la página que deseamos visualizar.

Ahora, efectuemos estas acciones explícitamente con SocketChannel. El siguiente programa, TechTipReader, utiliza SocketChannel para obtener la página. Nótese el método getTips() en el programa. Este método primero instancia un SocketChannel. Luego usa esa instancia para conectarse al puerto 80 de developer.java.sun.com. Después de esto, el método envía un pedido estándar HTTP "GET" para obtener la página JDCTechTips.

La respuesta del servidor se imprime por salida estándar.

   import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import java.net.InetSocketAddress;
import java.io.IOException;
public class TechTipReader {
private Charset charset =
Charset.forName("UTF-8");
private SocketChannel channel;
public void getTips() {
try {
connect();
sendRequest();
readResponse();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {}
}
}
}
private void connect() throws IOException {
InetSocketAddress socketAddress =
new InetSocketAddress(
"developer.java.sun.com", 80);
channel = SocketChannel.open(socketAddress);
}
private void sendRequest() throws IOException {
channel.write(charset.encode("GET "
+ "/developer/JDCTechTips/"
+ "rnrn"));
}
private void readResponse() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ((channel.read(buffer)) != -1) {
buffer.flip();
System.out.println(charset.decode(buffer));
buffer.clear();
}
}
public static void main(String[] args) {
new TechTipReader().getTips();
}
}

Observando más profundamente el programa TechTipReader, nótese que el método connect() crea una InetSocketAddress pasándole dos parámetros: un String que representa el dominio, y el puerto HTTP estándar 80. La llamada al método static SocketChannel.open() recibe la InetSocketAddress como parámetro. Esta llamada es equivalente a las siguientes dos sentencias:

   channel = SocketChannel.open();
channel.connect(socketAddress);

El método sendRequest() envía el pedido de tres líneas. La primera línea es "GET /developer/JDCTechTips/" y la segunda y tercera están vacías. La llamada a channel.write() recibe un ByteBuffer como argumento, por lo que debemos convertir la String. La mayor parte del tiempo empleada en trabajar con SocketChannels implica conversiones ida y vuelta (desde y hacia ByteBuffers). Existen varias maneras de realizar la conversión, este ejemplo usa el método encode() de la clase java.nio.charset.Charset.

El método readResponse() es responsable de manipular la respuesta retornada al SocketChannel. El método lee desde el SocketChannel hacia ByteBuffer. Mientras no se halla alcanzado el final del archivo que está siendo transferido, el método modifica el estado del buffer cambiándolo de leyendo, a preparado para escribir. Los contenidos del ByteBuffer son transformados por el método charset.decode() hacia una String que es enviada a salida estándar.

Cuando ejecutamos el programa TechTipReader, deberíamos obtener el HTML de la página.

Ahora observemos un segundo ejemplo. En este ejemplo, dos números son sumados. El ejemplo incluye un cliente y un servidor. Cuando el cliente tiene dos números para sumar, envía los números al servidor. El servidor entonces efectúa la adición y retorna la suma. Este ejemplo está basado en la API SocketChannel API. Otras posibles soluciones podrían emplear Web Services, RMI o Servlets.

En este ejemplo, los contenidos del ByteBuffer son dos enteros. Esto nos permite restringir el ByteBuffer a ocho bytes. También permite que podamos crear un IntBuffer que sirve como vista hacia el ByteBuffer, como sigue:

  private ByteBuffer buffer = ByteBuffer.allocate(8);
private IntBuffer intBuffer = buffer.asIntBuffer();

El siguiente programa, SumClient, provee la parte cliente del ejemplo. Para poder ejecutar SumClient, debemos primero inicializar la parte servidor del ejemplo, SumServer, que se puede encontrar más abajo en este mismo artículo.

   import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class SumClient {
private SocketChannel channel;
private ByteBuffer buffer = ByteBuffer.allocate(8);
private IntBuffer intBuffer = buffer.asIntBuffer();
public void getSum(int i, int j) {
try {
channel = connect();
sendSumRequest(i, j);
receiveResponse();
} catch (IOException e) {
// add exception handling code here
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
// add exception handling code here
e.printStackTrace();
}
}
}
}
private SocketChannel connect()
throws IOException {
InetSocketAddress socketAddress =
new InetSocketAddress("localhost", 9099);
return SocketChannel.open(socketAddress);
}
private void sendSumRequest(int i, int j)
throws IOException {
buffer.clear();
intBuffer.put(0, i);
intBuffer.put(1, j);
channel.write(buffer);
System.out.println("Sent request for sum of "
+ i + " and " + j + "...");
}
private void receiveResponse()
throws IOException {
buffer.clear();
channel.read(buffer);
System.out.println(
"Received response that sum is "
+ intBuffer.get(0) + ".");
}
public static void main(String[] args) {
new SumClient().getSum(14, 23);
}
}

El método getSum() en SumClient conecta un SocketChannel a una dirección predeterminada. El método connect() es esencialmente el mismo que el visto en el ejemplo TechTipReader, citado anteriormente. Luego de llamar a connect(), el método getSum() pasa dos enteros (en el ejemplo, 14 y 23) al método sendSumRequest(), que, en retribución, carga el ByteBuffer y escribe el contenido del SocketChannel. El método receiveResponse() entonces toma los contenidos del método y los escribe a salida estándar.

Cuando ejecutamos SumClient (luego de haber inicializado SumServer), debería desplegar lo siguiente:

   Sent request for sum of 14 and 23...
Received response that sum is 37.

Observando más profundamente SumClient, debe notarse que sendSumRequest() utiliza dos vistas diferentes del mismo ByteBuffer. Primero, buffer.clear() ignora la marca en el buffer, posiciona el mismo en el inicio, y setea el límite como su capacidad. En efecto limpia al buffer. Luego, el ByteBuffer es visto como un buffer que contiene dos enteros usando la manija (handle) intBuffer. El entero enviado como primer parámetro sendSumRequest() se inserta en el primer slot usando intBuffer.put(0,i), y el segundo parámetro es similarmente posicionado en el segundo slot. El método write() de SocketChannel recibe un ByteBuffer, entonces la primera vista del buffer es requerida.

El método receiveResponse() es casi un espejo de sendSumRequest(). El buffer es limpiado nuevamente. Entonces los contenidos de SocketChannel son leídos hacia la vista ByteBuffer del buffer. Luego, el IntBuffer es utilizado para retornar el entero ubicado en la primera posición.

Antes de observar el SumServer, la parte servidor de la solución, quizá sería útil insertar una verificación en aquellos lugares en donde llamamos a los métodos read() y write() en el objeto SocketChannel. Estos métodos retornan un long con el número de bytes leídos o escritos. En este caso, nosotros esperamos ocho bytes. Ubiquemos la siguiente línea dentro del método sendSumRequest():

   channel.write(buffer);

Reemplazémosla con la siguiente verificación sobre el tamaño del buffer que se está escribiendo.

   if (channel.write(buffer)!= 8){
throw new IOException("Expected 8 bytes.");
}

Insertemos una verificación similar alrededor de la llamada channel.read() en el método receiveResponse().

Aquí está el SumServer. Para que el ejemplo pueda ejecutarse, debemos inicializar un servidor que escuche pedidos de entrada en una ubicación determinada. En el método openChannel() citado a continuación, utilizamos el puerto 9099. Se efectúa una llamada al método estático open() de ServerSocketChannel, la siguiente línea liga (binds) el socket sobre el puerto especificado. Siempre que channel.isOpen() retorne true, un mensaje de confirmación se envía a la salida estándar.

   import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
public class SumServer {
ByteBuffer buffer = ByteBuffer.allocate(8);
IntBuffer intBuffer = buffer.asIntBuffer();
ServerSocketChannel channel = null;
SocketChannel sc = null;
public void start() {
try {
openChannel();
waitForConnection();
} catch (IOException e) {
e.printStackTrace();
}
}
private void openChannel() throws IOException {
channel = ServerSocketChannel.open();
channel.socket().bind(
new InetSocketAddress(9099));
while (!channel.isOpen()) {
}
System.out.println("Channel is open...");
}
private void waitForConnection()
throws IOException {
while (true) {
sc = channel.accept();
if (sc != null) {
System.out.println(
"A connection is added...");
processRequest();
sc.close();
}
}
}
private void processRequest() throws IOException {
buffer.clear();
sc.read(buffer);
int i = intBuffer.get(0);
int j = intBuffer.get(1);
buffer.flip();
System.out.println("Received request to add "
+ i + "  and " + j);
buffer.clear();
intBuffer.put(0, i + j);
sc.write(buffer);
System.out.println("Returned sum of " +
intBuffer.get(0) + ".");
}
public static void main(String[] args) {
new SumServer().start();
}
}

Cuando ejecutamos SumServer, debería desplegar lo siguiente:

   Channel is open...
Sent request for sum of 14 and 23...
Received response that sum is 37.

Luego, cuando ejecutamos SumClient, SumServer debería mostrar lo siguiente:

   A connection is added...
Received request to add 14  and 23
Returned sum of 37.

En el método waitForConnection(), la aplicación entra en un ciclo hasta que se recibe un pedido de conexión hacia el ServerSocketChannel. Cuando un pedido llega, channel.accept() retorna un SocketChannel, por lo que la variable sc ya no es más null. La petición entrante es procesada, y luego el SocketChannel se cierra. El servidor espera entonces por otra petición de conexión. Nótese que en este sencillo ejemplo las peticiones son procesadas enteramente una por vez Una estructura diferente se requeriría para un servidor que debe manejar múltiples peticiones concurrentes.

El método processRequest() es muy similar a los métodos correspondientes en SumClient. El buffer se limpia, el canal se lee hacia el buffer. Luego, la vista IntBuffer se utiliza para obtener los dos enteros contenidos en el buffer. El buffer luego se limpia y la suma se inserta en el mismo usando la vista IntBuffer. El buffer luego se escribe hacia el cliente utilizando SocketChannel.

Daniel H. Steinberg
http://cricava.com/java/detalleArticulos.php?id=71

One Comment

Leave A Comment?