1. Introducción
En este artículo, vamos a echar un vistazo a Netty, un marco de aplicación de red asincrónico controlado por eventos.
El objetivo principal de Netty es construir servidores de protocolo de alto rendimiento basados en NIO (o posiblemente NIO.2) con separación y acoplamiento flexible de la red y los componentes de lógica empresarial. Podría implementar un protocolo ampliamente conocido, como HTTP, o su propio protocolo específico.
2. Conceptos básicos
Netty es un marco sin bloqueo. Esto conduce a un alto rendimiento en comparación con el bloqueo de E / S. Comprender la E / S sin bloqueo es crucial para comprender los componentes centrales de Netty y sus relaciones.
2.1. Canal
El canal es la base de Java NIO. Representa una conexión abierta que es capaz de realizar operaciones IO como lectura y escritura.
2.2. Futuro
Cada operación de E / S en un canal en Netty es sin bloqueo.
Esto significa que cada operación se devuelve inmediatamente después de la llamada. Hay una interfaz Future en la biblioteca estándar de Java, pero no es conveniente para los propósitos de Netty; solo podemos preguntarle al Future sobre la finalización de la operación o bloquear el hilo actual hasta que la operación esté completa.
Es por eso que Netty tiene su propia interfaz ChannelFuture . Podemos pasar una devolución de llamada a ChannelFuture, que se llamará al finalizar la operación.
2.3. Eventos y controladores
Netty utiliza un paradigma de aplicación controlado por eventos, por lo que la canalización del procesamiento de datos es una cadena de eventos que pasan por los controladores. Los eventos y controladores pueden estar relacionados con el flujo de datos entrantes y salientes. Los eventos entrantes pueden ser los siguientes:
- Activación y desactivación de canales
- Leer eventos de operación
- Eventos de excepción
- Eventos de usuario
Los eventos salientes son más simples y, en general, están relacionados con la apertura / cierre de una conexión y la escritura / eliminación de datos.
Las aplicaciones de Netty constan de un par de eventos lógicos de aplicación y de red y sus controladores. Las interfaces base para los controladores de eventos del canal son ChannelHandler y sus antepasados ChannelOutboundHandler y ChannelInboundHandler .
Netty proporciona una enorme jerarquía de implementaciones de ChannelHandler. Vale la pena señalar los adaptadores que son solo implementaciones vacías, por ejemplo, ChannelInboundHandlerAdapter y ChannelOutboundHandlerAdapter . Podríamos extender estos adaptadores cuando necesitemos procesar solo un subconjunto de todos los eventos.
Además, hay muchas implementaciones de protocolos específicos como HTTP, por ejemplo , HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Sería bueno conocerlos en el Javadoc de Netty.
2.4. Codificadores y decodificadores
A medida que trabajamos con el protocolo de red, necesitamos realizar la serialización y deserialización de datos. Para este propósito, Netty introduce extensiones especiales de ChannelInboundHandler para decodificadores que son capaces de decodificar datos entrantes. La clase base de la mayoría de los decodificadores es ByteToMessageDecoder.
Para codificar datos salientes, Netty tiene extensiones de ChannelOutboundHandler llamadas codificadores. MessageToByteEncoder es la base para la mayoría de las implementaciones de codificadores . Podemos convertir el mensaje de una secuencia de bytes a un objeto Java y viceversa con codificadores y decodificadores.
3. Ejemplo de aplicación de servidor
Creemos un proyecto que represente un servidor de protocolo simple que recibe una solicitud, realiza un cálculo y envía una respuesta.
3.1. Dependencias
En primer lugar, debemos proporcionar la dependencia de Netty en nuestro pom.xml :
io.netty netty-all 4.1.10.Final
Podemos encontrar la última versión en Maven Central.
3.2. Modelo de datos
La clase de datos de la solicitud tendría la siguiente estructura:
public class RequestData { private int intValue; private String stringValue; // standard getters and setters }
Supongamos que el servidor recibe la solicitud y devuelve el valor intValue multiplicado por 2. La respuesta tendría el valor int único:
public class ResponseData { private int intValue; // standard getters and setters }
3.3. Solicitar decodificador
Ahora necesitamos crear codificadores y decodificadores para nuestros mensajes de protocolo.
Cabe señalar que Netty trabaja con un búfer de recepción de socket , que se representa no como una cola sino como un montón de bytes. Esto significa que se puede llamar a nuestro controlador de entrada cuando un servidor no recibe el mensaje completo.
Debemos asegurarnos de haber recibido el mensaje completo antes de procesarlo y hay muchas formas de hacerlo.
En primer lugar, podemos crear un ByteBuf temporal y agregarle todos los bytes entrantes hasta que obtengamos la cantidad requerida de bytes:
public class SimpleProcessingHandler extends ChannelInboundHandlerAdapter { private ByteBuf tmp; @Override public void handlerAdded(ChannelHandlerContext ctx) { System.out.println("Handler added"); tmp = ctx.alloc().buffer(4); } @Override public void handlerRemoved(ChannelHandlerContext ctx) { System.out.println("Handler removed"); tmp.release(); tmp = null; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; tmp.writeBytes(m); m.release(); if (tmp.readableBytes() >= 4) { // request processing RequestData requestData = new RequestData(); requestData.setIntValue(tmp.readInt()); ResponseData responseData = new ResponseData(); responseData.setIntValue(requestData.getIntValue() * 2); ChannelFuture future = ctx.writeAndFlush(responseData); future.addListener(ChannelFutureListener.CLOSE); } } }
El ejemplo que se muestra arriba parece un poco extraño pero nos ayuda a comprender cómo funciona Netty. Cada método de nuestro controlador se llama cuando ocurre su evento correspondiente. Entonces, inicializamos el búfer cuando se agrega el controlador, lo llenamos con datos al recibir nuevos bytes y comenzamos a procesarlo cuando obtenemos suficientes datos.
We deliberately did not use a stringValue — decoding in such a manner would be unnecessarily complex. That's why Netty provides useful decoder classes which are implementations of ChannelInboundHandler: ByteToMessageDecoder and ReplayingDecoder.
As we noted above we can create a channel processing pipeline with Netty. So we can put our decoder as the first handler and the processing logic handler can come after it.
The decoder for RequestData is shown next:
public class RequestDecoder extends ReplayingDecoder { private final Charset charset = Charset.forName("UTF-8"); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { RequestData data = new RequestData(); data.setIntValue(in.readInt()); int strLen = in.readInt(); data.setStringValue( in.readCharSequence(strLen, charset).toString()); out.add(data); } }
An idea of this decoder is pretty simple. It uses an implementation of ByteBuf which throws an exception when there is not enough data in the buffer for the reading operation.
When the exception is caught the buffer is rewound to the beginning and the decoder waits for a new portion of data. Decoding stops when the out list is not empty after decode execution.
3.4. Response Encoder
Besides decoding the RequestData we need to encode the message. This operation is simpler because we have the full message data when the write operation occurs.
We can write data to Channel in our main handler or we can separate the logic and create a handler extending MessageToByteEncoder which will catch the write ResponseData operation:
public class ResponseDataEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, ResponseData msg, ByteBuf out) throws Exception { out.writeInt(msg.getIntValue()); } }
3.5. Request Processing
Since we carried out the decoding and encoding in separate handlers we need to change our ProcessingHandler:
public class ProcessingHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { RequestData requestData = (RequestData) msg; ResponseData responseData = new ResponseData(); responseData.setIntValue(requestData.getIntValue() * 2); ChannelFuture future = ctx.writeAndFlush(responseData); future.addListener(ChannelFutureListener.CLOSE); System.out.println(requestData); } }
3.6. Server Bootstrap
Now let's put it all together and run our server:
public class NettyServer { private int port; // constructor public static void main(String[] args) throws Exception { int port = args.length > 0 ? Integer.parseInt(args[0]); : 8080; new NettyServer(port).run(); } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RequestDecoder(), new ResponseDataEncoder(), new ProcessingHandler()); } }).option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
The details of the classes used in the above server bootstrap example can be found in their Javadoc. The most interesting part is this line:
ch.pipeline().addLast( new RequestDecoder(), new ResponseDataEncoder(), new ProcessingHandler());
Here we define inbound and outbound handlers that will process requests and output in the correct order.
4. Client Application
The client should perform reverse encoding and decoding, so we need to have a RequestDataEncoder and ResponseDataDecoder:
public class RequestDataEncoder extends MessageToByteEncoder { private final Charset charset = Charset.forName("UTF-8"); @Override protected void encode(ChannelHandlerContext ctx, RequestData msg, ByteBuf out) throws Exception { out.writeInt(msg.getIntValue()); out.writeInt(msg.getStringValue().length()); out.writeCharSequence(msg.getStringValue(), charset); } }
public class ResponseDataDecoder extends ReplayingDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { ResponseData data = new ResponseData(); data.setIntValue(in.readInt()); out.add(data); } }
Also, we need to define a ClientHandler which will send the request and receive the response from server:
public class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { RequestData msg = new RequestData(); msg.setIntValue(123); msg.setStringValue( "all work and no play makes jack a dull boy"); ChannelFuture future = ctx.writeAndFlush(msg); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println((ResponseData)msg); ctx.close(); } }
Now let's bootstrap the client:
public class NettyClient { public static void main(String[] args) throws Exception { String host = "localhost"; int port = 8080; EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); b.handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RequestDataEncoder(), new ResponseDataDecoder(), new ClientHandler()); } }); ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
Como podemos ver, hay muchos detalles en común con el bootstrapping del servidor.
Ahora podemos ejecutar el método principal del cliente y echar un vistazo a la salida de la consola. Como era de esperar, obtuvimos ResponseData con intValue igual a 246.
5. Conclusión
En este artículo, tuvimos una rápida introducción a Netty. Mostramos sus componentes principales, como Channel y ChannelHandler . Además, hemos creado un servidor de protocolo simple sin bloqueo y un cliente para él.
Como siempre, todos los ejemplos de código están disponibles en GitHub.