Introdução ao Netty
*1. Introdução *
Neste artigo, veremos o Netty - uma estrutura de aplicativo de rede assíncrona orientada a eventos.
O principal objetivo do Netty é criar servidores de protocolo de alto desempenho baseados em NIO (ou possivelmente NIO.2) com separação e acoplamento livre dos componentes da rede e da lógica de negócios. Pode implementar um protocolo amplamente conhecido, como HTTP, ou seu próprio protocolo específico.
===* 2. Conceitos principais *
Netty é uma estrutura sem bloqueio. Isso leva a um alto rendimento comparado ao bloqueio de E/S.* Compreender as E/S sem bloqueio é crucial para entender os principais componentes da Netty e seus relacionamentos. *
====* 2.1 Canal *
Channel é a base do Java NIO. Representa uma conexão aberta capaz de operações de E/S, como leitura e gravação.
====* 2.2 Futuro*
*Todas as operações de E/S em um _Channel_ no Netty são sem bloqueio.*
Isso significa que todas as operações são retornadas imediatamente após a chamada. Há uma interface Future na biblioteca Java padrão, mas não é conveniente para fins do Netty - podemos perguntar ao Future sobre a conclusão da operação ou bloquear o encadeamento atual até que a operação seja concluída.
É por isso que * Netty tem sua própria interface ChannelFuture * * *Podemos passar um retorno de chamada para ChannelFuture, que será chamado após a conclusão da operação.
====* 2.3 Eventos e Manipuladores *
O Netty usa um paradigma de aplicativo orientado a eventos; portanto, o pipeline do processamento de dados é uma cadeia de eventos passando por manipuladores. Eventos e manipuladores podem estar relacionados ao fluxo de dados de entrada e saída. Eventos de entrada podem ser os seguintes:
-
Ativação e desativação de canal
-
Ler eventos de operação
-
Eventos de exceção *Eventos do usuário
Os eventos de saída são mais simples e, geralmente, estão relacionados à abertura/fechamento de uma conexão e gravação/liberação de dados.
Os aplicativos Netty consistem em alguns eventos lógicos de rede e aplicativos e seus manipuladores. As interfaces base para os manipuladores de eventos do canal são ChannelHandler e seus ancestrais ChannelOutboundHandler e ChannelInboundHandler.
O Netty fornece uma enorme hierarquia de implementações de ChannelHandler. Vale a pena notar os adaptadores que são apenas implementações vazias, por exemplo. ChannelInboundHandlerAdapter e ChannelOutboundHandlerAdapter. Poderíamos estender esses adaptadores quando precisarmos processar apenas um subconjunto de todos os eventos.
Além disso, existem muitas implementações de protocolos específicos, como HTTP, por exemplo, HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Seria bom se familiarizar com eles no Javadoc de Netty.
====* 2.4. Codificadores e decodificadores *
Enquanto trabalhamos com o protocolo de rede, precisamos executar serialização e desserialização de dados. Para esse propósito, o Netty introduz extensões especiais do ChannelInboundHandler para* decodificadores *que são capazes de decodificar os dados recebidos. A classe base da maioria dos decodificadores é ByteToMessageDecoder.
Para codificar dados de saída, o Netty possui extensões do ChannelOutboundHandler chamadas* codificadores. *MessageToByteEncoder é a base para a maioria das implementações de codificadores . Podemos converter a mensagem da sequência de bytes em objeto Java e vice-versa com codificadores e decodificadores.
===* 3. Aplicativo de servidor de exemplo *
Vamos criar um projeto que representa um servidor de protocolo simples que recebe uma solicitação, executa um cálculo e envia uma resposta.
====* 3.1 Dependências *
Primeiro, precisamos fornecer a dependência do Netty em nosso pom.xml:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.10.Final</version>
</dependency>
Podemos encontrar a versão mais recente em em Maven Central.
====* 3.2 Modelo de dados *
A classe de dados da solicitação teria a seguinte estrutura:
public class RequestData {
private int intValue;
private String stringValue;
//standard getters and setters
}
Vamos supor que o servidor receba a solicitação e retorne o intValue multiplicado por 2. A resposta teria o valor int único:
public class ResponseData {
private int intValue;
//standard getters and setters
}
====* 3.3 Decodificador de solicitação *
Agora precisamos criar codificadores e decodificadores para nossas mensagens de protocolo.
Deve-se notar que* Netty trabalha com buffer de recebimento de soquete *, que é representado não como uma fila, mas apenas como um monte de bytes. Isso significa que nosso manipulador de entrada pode ser chamado quando a mensagem completa não for recebida por um servidor.
*Devemos garantir que recebemos a mensagem completa antes do processamento* e há muitas maneiras de fazer isso.
Primeiro, podemos criar um ByteBuf temporário e anexar a ele todos os bytes de entrada até obtermos a quantidade necessária 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);
}
}
}
O exemplo mostrado acima parece um pouco estranho, mas nos ajuda a entender como o Netty funciona. Todo método de nosso manipulador é chamado quando seu evento correspondente ocorre. Portanto, inicializamos o buffer quando o manipulador é adicionado, preenchemos com dados ao receber novos bytes e começamos a processá-lo quando obtemos dados suficientes.
Deliberadamente, não usamos um stringValue - a decodificação dessa maneira seria desnecessariamente complexa. É por isso que o Netty fornece classes úteis de decodificador que são implementações de ChannelInboundHandler: ByteToMessageDecoder e ReplayingDecoder.
Como observamos acima, podemos criar um pipeline de processamento de canal com o Netty. Portanto, podemos colocar nosso decodificador como o primeiro manipulador e o manipulador da lógica de processamento pode vir depois dele.
O decodificador para RequestData é mostrado a seguir:
public class RequestDecoder extends ReplayingDecoder<RequestData> {
private final Charset charset = Charset.forName("UTF-8");
@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf in, List<Object> 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);
}
}
Uma idéia desse decodificador é bem simples. Ele usa uma implementação de ByteBuf que lança uma exceção quando não há dados suficientes no buffer para a operação de leitura.
Quando a exceção é capturada, o buffer é rebobinado para o início e o decodificador aguarda uma nova parte dos dados. A decodificação para quando a lista out não está vazia após a execução do decode.
====* 3.4 Codificador de resposta *
Além de decodificar o RequestData, precisamos codificar a mensagem. Essa operação é mais simples porque temos os dados completos da mensagem quando a operação de gravação ocorre.
Podemos gravar dados em Channel em nosso manipulador principal ou podemos separar a lógica e criar um manipulador estendendo MessageToByteEncoder que capturará a operação de gravação ResponseData:
public class ResponseDataEncoder
extends MessageToByteEncoder<ResponseData> {
@Override
protected void encode(ChannelHandlerContext ctx,
ResponseData msg, ByteBuf out) throws Exception {
out.writeInt(msg.getIntValue());
}
}
====* 3.5 Processamento de Solicitações *
Como realizamos a decodificação e a codificação em manipuladores separados, precisamos alterar nosso 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 Inicialização do servidor
Agora vamos juntar tudo e executar nosso servidor:
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<SocketChannel>() {
@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();
}
}
}
Os detalhes das classes usadas no exemplo de inicialização do servidor acima podem ser encontrados em seu Javadoc. A parte mais interessante é esta linha:
ch.pipeline().addLast(
new RequestDecoder(),
new ResponseDataEncoder(),
new ProcessingHandler());
Aqui, definimos manipuladores de entrada e saída que processarão solicitações e saída na ordem correta.
*4. Aplicativo cliente *
O cliente deve executar a codificação e decodificação reversa, portanto, precisamos ter um RequestDataEncoder e ResponseDataDecoder:
public class RequestDataEncoder
extends MessageToByteEncoder<RequestData> {
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<ResponseData> {
@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf in, List<Object> out) throws Exception {
ResponseData data = new ResponseData();
data.setIntValue(in.readInt());
out.add(data);
}
}
Além disso, precisamos definir um ClientHandler que enviará a solicitação e receberá a resposta do servidor:
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();
}
}
Agora vamos inicializar o cliente:
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<SocketChannel>() {
@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, há muitos detalhes em comum com a inicialização do servidor.
Agora podemos executar o método principal do cliente e dar uma olhada na saída do console. Como esperado, obtivemos ResponseData com intValue igual a 246.
===* 5. Conclusão*
Neste artigo, tivemos uma rápida introdução ao Netty. Mostramos seus componentes principais, como Channel e ChannelHandler. Além disso, criamos um servidor de protocolo sem bloqueio simples e um cliente para ele.
Como sempre, todos os exemplos de código estão disponíveis over no GitHub.