Diferencia entre revisiones de «Ejemplo Sencillo de TCP/IP con netty»

De ChuWiki
Saltar a: navegación, buscar
(El cliente)
(El cliente)
Línea 88: Línea 88:
 
* (5) Con sucesivas llamadas a option() podemos pasar todas las opciones relativas a socket tcp/ip que queramos.
 
* (5) Con sucesivas llamadas a option() podemos pasar todas las opciones relativas a socket tcp/ip que queramos.
 
* (6) Llamada a connect() para establecer la conexión. Debemos indicar IP del servidor (localhost en el ejemplo) y puerto de escucha del servidor (8080 en el ejemplo). La llamada a connect() vuelve inmediatamente, con sync() esperamos a que la conexión esté realmente establecida.
 
* (6) Llamada a connect() para establecer la conexión. Debemos indicar IP del servidor (localhost en el ejemplo) y puerto de escucha del servidor (8080 en el ejemplo). La llamada a connect() vuelve inmediatamente, con sync() esperamos a que la conexión esté realmente establecida.
 +
 +
== Eventos ==
 +
 +
Hasta ahora lo necesario para abrir los sockets y conectarlos. Como se comentó, el código es más o menos siempre el mismo, con pequeños ajustes como pueden ser las opciones que deseemos para los sockets, puerto o ip. Por supuesto, si tenemos una aplicación más elaborada o exigente, se pueden ajustar más cosas, como el número de hilos en los bucles de eventos, el tipo de clase a usar para el socket servidor o cliente, etc.
 +
 +
Pero vamos ahora a la "chicha", como enterarnos de los mensajes que llegan por el socket y como enviarlos. Dijimos antes que esto era algo que hacian las clases serverHandler y clientHandler. Vamos a ver aquí los coneptos básicos y hacer un ejemplo muy simple.
 +
 +
Cada vez que se produzca algún tipo de evento en el socket, netty avisará a los handler que se le pasen. Los handler deben implementar determinada interface. Hay dos posibles : ChannelInboundHandler y ChannelOutboundHandler.
 +
 +
=== ChannelInboundHandler ===
 +
 +
Esta interface deben implementarla aquellas clases que estén interesadas en eventos que se producen en nuestro socket provocados desde fuera. Hay muchos eventos pero los más interesantes puede ser :
 +
* Se conecta o desconecta un cliente o se abre o cierra nuestro socket.
 +
* Llega un mensaje (bytes) del socket.
 +
 +
=== ChannelOutboundHandler ===
 +
 +
Esta interfaz deben implementarla aquellas clases que estén interesadas en eventos que se producen en nuestro socket desde nuestro de  nuestra aplicación. Con este handler podemos enterarnos de :
 +
* Cuando se hace el bind(), el connect() o el close()
 +
* Los write() o read() de nuestra aplicación (habitualmente nos intersa el write()).
 +
 +
== Lectura de mensajes ==
 +
 +
Para leer mensajes, debemos hacer un handler que implemente la interfaz ChannelInboundHandler e implementar su método channelRead(). Netty llamará a este método cada vez que tenga bytes leídos por el socket y nos los pasará para que los tratemos. Como la interface ChannelInboundHandler tiene muchos métodos y somos vagos y no queremos implementarlos todos, heredaremos de ChannelInboundHandlerAdapter, y solo sobreescribiremos el método que hemos dicho
 +
 +
<syntaxhighlight lang="java">
 +
public class ServerHandler extends ChannelInboundHandlerAdapter {
 +
    @Override
 +
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {      // (1)
 +
 +
        ByteBuf buf =(ByteBuf)msg;    // (2)
 +
        String text = buf.toString(Charset.defaultCharset());  // (3)
 +
        System.out.print(text);
 +
       
 +
        buf.release(); // (4)
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
Vamos con los detalles
 +
 +
* (1) En el método recibimos un contexto de la conexión (ChannelHandlerContext) que tiene su utilidad, pero no para este caso simple. También recibmos los bytes leídos como Object msg.
 +
* (2) En este ejemplo sencillo, lo recibido en un ByteBuf de netty, con los bytes leídos en el socket, así que hacemos el cast. En ejemplos más complejos de netty, este Object puede ser cualquier cosa, incluso clases nuestras de datos que hemos obtenido en pasos previos a partir de los bytes leídos.
 +
* (3) Como en el ejemplo el cliente enviará texto, convertimos estos bytes a texto y los sacamos por pantalla.
 +
* (4) Netty, para ser eficiente, no crea ByteBuf alegremente, sino que tiene unos cuantos creados y los reutiliza. Cuando ya no nos haga falta, debemos decirle que lo puede liberar para usarlo en otro sitio. El responsable de liberar estos buffer es el que los recibe y no los retransmite a otro sitio. Ese es nuestro caso, convertimos al buffer a String, lo sacamos por pantalla y no llamamos a nadie pasandole al buffer, así que nos toca liberarlo. No nos tocaría si después de sacar por pantalla hicieramos una llamada a alguien pasándole este buffer, por ejemplo tomaTuElBuffferTodoParaTi(buf). En esta caso sería responsabilidad del que lo recibe el liberarlo si no pasa la pelota a otro.
 +
 +
== Escribir mensajes ==
 +
 +
Vamos primero con el caso del cliente, que es más sencillo. El cliente sólo envía el mensaje al servidor. El servidor tendría una lógica más complicada, porque debe enviar el mensaje a uno o más clientes y tendría que tener alguna lógica para decidir a cuales.
 +
 +
Para el envío, nos vale nuevamente con implmentar la interface ChannelInboundHandler (o heredar de ChannelInboundHandlerAdapter mejor) y tenemos que hacer algo como esto
 +
 +
<syntaxhighlight lang="java">
 +
public class ClientHandler extends ChannelInboundHandlerAdapter{
 +
    private ChannelHandlerContext ctx;  // (1)
 +
 +
    @Override
 +
    public void channelActive(ChannelHandlerContext ctx) throws Exception {  // (2)
 +
        this.ctx=ctx;
 +
    }
 +
 +
    @Override
 +
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {  // (3)
 +
        ctx=null;
 +
    }
 +
 +
    public void sendMessage(String msg){  // (4)
 +
        if (null==ctx)
 +
            return;
 +
        ByteBuf buf = ctx.alloc().directBuffer();  // (5)
 +
        buf.writeCharSequence(msg,Charset.defaultCharset());
 +
        ctx.write(buf);
 +
        ctx.flush();
 +
    }
 +
}
 +
</syntaxhighlight>

Revisión del 16:09 1 ago 2018

Netty es una librería Java que nos facilita todo el uso de sockets. Está pensada para ser eficiente y consumir pocos recursos, además de tener ya implementados bastantes protocolos estándar, ahorrándonos el trabajo de hacerlo a nosotros o buscar otras librerías.

Veamos aquí un ejemplo básico de cómo hacer un socket TCP/IP. Haremos un servidor que admitirá varios clientes, cada cliente enviará un texto periodicamente y el servidor se lo reenviará al resto de clientes. Lo haremos con lo básico de Netty, sin utilizar ninguno de los encoder o decoder que netty nos proporiciona.

Aquí tienes el código de ejemplo de socket tcp/ip con netty

El Servidor

En netty el código para abrir un socket servidor TCP/IP es más o menos siempre el mismo, copy-paste y lo explicamos un poco

int port = 8080; // El puerto que queramos

EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup(); // (2)

try {
   ServerBootstrap b = new ServerBootstrap(); // (3)
   b.group(bossGroup, workerGroup)  // (4)
      .channel(NioServerSocketChannel.class) // (5)
      .childHandler(new ChannelInitializer<SocketChannel>() { // (6)
          @Override
          public void initChannel(SocketChannel ch) throws Exception { // (7)
             ch.pipeline().addLast(serverHandler); // (8)
          }
      })
      .option(ChannelOption.SO_BACKLOG, 128)          // (9)
      .childOption(ChannelOption.SO_KEEPALIVE, true); // (10)
       
   ChannelFuture f = b.bind(port).sync(); // (11)

   f.channel().closeFuture().sync(); // (12)

Veamos todo esto

  • (1) Netty está orientado a eventos. Se producen eventos y se propagan cuando se abre una conexión, cuando se acepta un cliente, cuando se cierra una conexión, cuando llega un mensaje, etc, etc, etc. Todos estos eventos se tratan en un bucle de eventos en una serie de hilos que maneja netty. Netty tiene varias clases que implementan estos bucles de eventos con hilos, entre ellas NioEventLoopGroup. Así que lo primero que hacemos es crear una de estas para los eventos del socket servidor. Cada vez que se acepte una conexión de un cliente o se cierre, será este bucle de eventos en el encargado de detectarlo y avisar a todos los que tengan interés en ello.
  • (2) Idem a (1), pero es el bucle de eventos para los eventos de clientes ya conectados, cuando se envían o reciben mensajes de ellos, también cuando se desconecta un cliente concreto, etc. Resumiendo, (1) es para los eventos del socket servidor y (2) para los eventos en cada uno de los socket con los clientes.
  • (3) ServerBootstrap es una clase que nos abre un socket servidor de forma fácil. Podríamos hacerlo usando clases de más bajo nivel de netty (ServerSocketChannel) y así tendriamos más control y libertad para crearlo como queramos, pero en general no es necesario. Basta con instanciar esta clase.
  • (4) Le pasamos los dos bucles de eventos que creamos antes, el del servidor para que acepte conexiones y el de los eventos de cada uno de los clientes ya conectados.
  • (5) Estamos haciendo un Socket Servidor que acepte conexiones de clientes. Netty tiene varias clases que permiten hacer esto, aquí decimos la que queremos usar, NioServerSocketChannel. Esta usa la función select de java.nio. Hay otras implementaciones que usan entradas/salidas bloqueantes, o que usan epoll de linux y que sólo funcionan en linux. Para un java moderno, NioServerSocketChannel es una buena elección.
  • (6) Llamamos al método childHandler pasándole un ChannelInitializer. Cada vez que un cliente se conecte, se llamará a (7) ChannelInitializer.initChannel() pasándole el SocketChannel con el cliente que se acaba de conectar, es decir, la conexión con dicho cliente. Aquí es donde tenemos que hacer cosas de nuestra cosecha para tratar los mensajes que nos envíe el cliente o para enviarle mensajes al cliente.
  • (8) Cada vez que sucede algo con el cliente (llega un mensaje, le enviamos un mensaje, se conecta, se desconecta, etc), netty provoca eventos a los que nos avisa si nos suscribimos. en la línea (8) es donde lo hacemos. En la llamada a ch.pipeline().addLast() pasamos una clase nuestra, serverHandler, que implementa la interfaz de netty correspondiente para recibir todos los eventos que nos interesen en la conexión con el cliente. Veremos esto con más detalle más adelante.
  • (9) Con el método option() podemos pasar todas las opciones que queramos típicas de un socket servidor TCP/IP. El método option() afecta al socket servidor.
  • (10) con el método childOption() podemos pasar todas las opciones que queramos típicas de un socket servidor TCP/IP. El método childOption() afecta a los socket que se abran con el cliente, no al socket servidor.
  • (11) Hacemos el típico bind() del socket servidor, indicando el puerto que queremos. La llamada a bind() vuelve inmediatamente, con la llamada a sync() el código se queda aquí bloqueado hasta que el servidor está en marcha.
  • (12) f.channel() nos devuelve el canal (conexión) que hay en el socket servidor. closeFuture() nos devuelve una "cosa" (un future) que es algo que sucederá pero todavía no ha sucedido y ese algo, en este caso, es que se cierre el socket servidor. Si llamamos al método sync() de esa cosa futura, el método sync() se quedará bloqueado hasta que esa cosa futura (el cierre del socket) sea realidad. Como nadie cierra el socket (en este ejemplo), este sync() se quedará aquí para siempre. Si en algún sitio llamaramos a f.channel().close(), entonces se cerraría el socket y el sync() acabaría saliendo.

El cliente

Para abrir un socket cliente en otro PC que se conecte a nuestro servidor, el código es como este pero algo más sencillo.

int port = 8080;

EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
   Bootstrap b = new Bootstrap(); // (1)
   b.group(workerGroup)
      .channel(NioSocketChannel.class) // (2)
      .handler(new ChannelInitializer<SocketChannel>() { // (3)
          @Override
          public void initChannel(SocketChannel ch) throws Exception {
             ch.pipeline().addLast(clientHandler);  // (4)
          }
      })
     .option(ChannelOption.SO_KEEPALIVE, true); // (5)

     // connect.
     ChannelFuture f = b.connect("localhost", port).sync(); // (6)

     // Wait until the server socket is closed.
     // In this example, this does not happen, but you can do that to gracefully
     // shut down your server.
     f.channel().closeFuture().sync();

Es algo más sencillo, comentamos sólo las diferencias

  • (1) Como es un cliente, usamos la clase de utilidad Bootstrap en vez de ServerBootstrap. Como vimos antes, se podría hacer usando clases de más bajo nivel de Netty, pero esta nos ayuda y es más fácil.
  • (2) Como es cliente, usamos la clase NioSocketChannel en vez de NioServerSocketChannel. Al igual que en el caso del servidor, netty nos ofrece varias implementaciones usando diferentes técnicas, pero NioSocketChannel es una opción adecuada para una versión de java moderna con la api nio.
  • (3) Aquí usamos el método handler() para pasarle el ChannelInitializer. En el servidor usabamos childHandler() porque eran handler para los sockets con los clientes y no para el socker servidor en sí mismo.
  • (4) Como en el caso anterior, aquí es donde debemos pasar un handler encargado de enviar o tratar mensajes del servidor. clientHandler es una instancia de una clase de nuestro código que veremos más adelante.
  • (5) Con sucesivas llamadas a option() podemos pasar todas las opciones relativas a socket tcp/ip que queramos.
  • (6) Llamada a connect() para establecer la conexión. Debemos indicar IP del servidor (localhost en el ejemplo) y puerto de escucha del servidor (8080 en el ejemplo). La llamada a connect() vuelve inmediatamente, con sync() esperamos a que la conexión esté realmente establecida.

Eventos

Hasta ahora lo necesario para abrir los sockets y conectarlos. Como se comentó, el código es más o menos siempre el mismo, con pequeños ajustes como pueden ser las opciones que deseemos para los sockets, puerto o ip. Por supuesto, si tenemos una aplicación más elaborada o exigente, se pueden ajustar más cosas, como el número de hilos en los bucles de eventos, el tipo de clase a usar para el socket servidor o cliente, etc.

Pero vamos ahora a la "chicha", como enterarnos de los mensajes que llegan por el socket y como enviarlos. Dijimos antes que esto era algo que hacian las clases serverHandler y clientHandler. Vamos a ver aquí los coneptos básicos y hacer un ejemplo muy simple.

Cada vez que se produzca algún tipo de evento en el socket, netty avisará a los handler que se le pasen. Los handler deben implementar determinada interface. Hay dos posibles : ChannelInboundHandler y ChannelOutboundHandler.

ChannelInboundHandler

Esta interface deben implementarla aquellas clases que estén interesadas en eventos que se producen en nuestro socket provocados desde fuera. Hay muchos eventos pero los más interesantes puede ser :

  • Se conecta o desconecta un cliente o se abre o cierra nuestro socket.
  • Llega un mensaje (bytes) del socket.

ChannelOutboundHandler

Esta interfaz deben implementarla aquellas clases que estén interesadas en eventos que se producen en nuestro socket desde nuestro de nuestra aplicación. Con este handler podemos enterarnos de :

  • Cuando se hace el bind(), el connect() o el close()
  • Los write() o read() de nuestra aplicación (habitualmente nos intersa el write()).

Lectura de mensajes

Para leer mensajes, debemos hacer un handler que implemente la interfaz ChannelInboundHandler e implementar su método channelRead(). Netty llamará a este método cada vez que tenga bytes leídos por el socket y nos los pasará para que los tratemos. Como la interface ChannelInboundHandler tiene muchos métodos y somos vagos y no queremos implementarlos todos, heredaremos de ChannelInboundHandlerAdapter, y solo sobreescribiremos el método que hemos dicho

public class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {      // (1)

        ByteBuf buf =(ByteBuf)msg;    // (2)
        String text = buf.toString(Charset.defaultCharset());   // (3)
        System.out.print(text);
        
        buf.release(); // (4)
    }
}

Vamos con los detalles

  • (1) En el método recibimos un contexto de la conexión (ChannelHandlerContext) que tiene su utilidad, pero no para este caso simple. También recibmos los bytes leídos como Object msg.
  • (2) En este ejemplo sencillo, lo recibido en un ByteBuf de netty, con los bytes leídos en el socket, así que hacemos el cast. En ejemplos más complejos de netty, este Object puede ser cualquier cosa, incluso clases nuestras de datos que hemos obtenido en pasos previos a partir de los bytes leídos.
  • (3) Como en el ejemplo el cliente enviará texto, convertimos estos bytes a texto y los sacamos por pantalla.
  • (4) Netty, para ser eficiente, no crea ByteBuf alegremente, sino que tiene unos cuantos creados y los reutiliza. Cuando ya no nos haga falta, debemos decirle que lo puede liberar para usarlo en otro sitio. El responsable de liberar estos buffer es el que los recibe y no los retransmite a otro sitio. Ese es nuestro caso, convertimos al buffer a String, lo sacamos por pantalla y no llamamos a nadie pasandole al buffer, así que nos toca liberarlo. No nos tocaría si después de sacar por pantalla hicieramos una llamada a alguien pasándole este buffer, por ejemplo tomaTuElBuffferTodoParaTi(buf). En esta caso sería responsabilidad del que lo recibe el liberarlo si no pasa la pelota a otro.

Escribir mensajes

Vamos primero con el caso del cliente, que es más sencillo. El cliente sólo envía el mensaje al servidor. El servidor tendría una lógica más complicada, porque debe enviar el mensaje a uno o más clientes y tendría que tener alguna lógica para decidir a cuales.

Para el envío, nos vale nuevamente con implmentar la interface ChannelInboundHandler (o heredar de ChannelInboundHandlerAdapter mejor) y tenemos que hacer algo como esto

public class ClientHandler extends ChannelInboundHandlerAdapter{
    private ChannelHandlerContext ctx;   // (1)

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {  // (2)
        this.ctx=ctx;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {  // (3)
        ctx=null;
    }

    public void sendMessage(String msg){  // (4)
        if (null==ctx)
            return;
        ByteBuf buf = ctx.alloc().directBuffer();  // (5)
        buf.writeCharSequence(msg,Charset.defaultCharset());
        ctx.write(buf);
        ctx.flush();
    }
}