Introduction à Netty

1. Introduction

Dans cet article, nous allons jeter un coup d'œil à Netty - une infrastructure d'application réseau asynchrone basée sur les événements.

L'objectif principal de Netty est de créer des serveurs de protocole haute performance basés sur NIO (ou éventuellement NIO.2) avec séparation et couplage lâche du réseau et des composants de logique métier. Il peut implémenter un protocole largement connu, tel que HTTP, ou votre propre protocole spécifique.

2. Concepts de base

Netty est un framework non bloquant. Cela conduit à un débit élevé par rapport au blocage des E / S. Comprendre les E / S non bloquantes est essentiel pour comprendre les composants de base de Netty et leurs relations.

2.1. Canal

Channel est la base de Java NIO. Il représente une connexion ouverte qui est capable d'opérations d'E / S telles que la lecture et l'écriture.

2.2. Avenir

Chaque opération d'E / S sur un canal dans Netty est non bloquante.

Cela signifie que chaque opération est renvoyée immédiatement après l'appel. Il existe une interface Future dans la bibliothèque Java standard, mais ce n'est pas pratique pour les besoins de Netty - nous ne pouvons que demander au Future l'achèvement de l'opération ou bloquer le thread actuel jusqu'à ce que l'opération soit terminée.

C'est pourquoi Netty a sa propre interface ChannelFuture . Nous pouvons passer un rappel à ChannelFuture qui sera appelé à la fin de l'opération.

2.3. Événements et gestionnaires

Netty utilise un paradigme d'application piloté par les événements, de sorte que le pipeline du traitement des données est une chaîne d'événements passant par des gestionnaires. Les événements et les gestionnaires peuvent être liés au flux de données entrant et sortant. Les événements entrants peuvent être les suivants:

  • Activation et désactivation des canaux
  • Lire les événements d'opération
  • Événements d'exception
  • Evénements utilisateur

Les événements sortants sont plus simples et, généralement, sont liés à l'ouverture / la fermeture d'une connexion et à l'écriture / au vidage des données.

Les applications Netty se composent de quelques événements de mise en réseau et de logique applicative et de leurs gestionnaires. Les interfaces de base pour les gestionnaires d'événements de canal sont ChannelHandler et ses ancêtres ChannelOutboundHandler et ChannelInboundHandler .

Netty fournit une énorme hiérarchie d'implémentations de ChannelHandler. Il convient de noter les adaptateurs qui ne sont que des implémentations vides, par exemple ChannelInboundHandlerAdapter et ChannelOutboundHandlerAdapter . Nous pouvons étendre ces adaptateurs lorsque nous devons traiter uniquement un sous-ensemble de tous les événements.

En outre, il existe de nombreuses implémentations de protocoles spécifiques tels que HTTP, par exemple HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Ce serait bien de faire connaissance avec eux dans le Javadoc de Netty.

2.4. Encodeurs et décodeurs

Lorsque nous travaillons avec le protocole réseau, nous devons effectuer la sérialisation et la désérialisation des données. À cette fin, Netty introduit des extensions spéciales du ChannelInboundHandler pour les décodeurs capables de décoder les données entrantes. La classe de base de la plupart des décodeurs est ByteToMessageDecoder.

Pour encoder les données sortantes, Netty a des extensions de ChannelOutboundHandler appelées encodeurs. MessageToByteEncoder est la base de la plupart des implémentations d'encodeur . Nous pouvons convertir le message de la séquence d'octets en objet Java et vice versa avec des encodeurs et des décodeurs.

3. Exemple d'application serveur

Créons un projet représentant un serveur de protocole simple qui reçoit une requête, effectue un calcul et envoie une réponse.

3.1. Dépendances

Tout d'abord, nous devons fournir la dépendance Netty dans notre pom.xml :

 io.netty netty-all 4.1.10.Final 

Nous pouvons trouver la dernière version sur Maven Central.

3.2. Modèle de données

La classe de données de demande aurait la structure suivante:

public class RequestData { private int intValue; private String stringValue; // standard getters and setters }

Supposons que le serveur reçoive la requête et renvoie la valeur intValue multipliée par 2. La réponse aurait la valeur unique int:

public class ResponseData { private int intValue; // standard getters and setters }

3.3. Demander un décodeur

Nous devons maintenant créer des encodeurs et des décodeurs pour nos messages de protocole.

It should be noted that Netty works with socket receive buffer, which is represented not as a queue but just as a bunch of bytes. This means that our inbound handler can be called when the full message is not received by a server.

We must make sure that we have received the full message before processing and there are many ways to do that.

First of all, we can create a temporary ByteBuf and append to it all inbound bytes until we get the required amount of 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); } } }

The example shown above looks a bit weird but helps us to understand how Netty works. Every method of our handler is called when its corresponding event occurs. So we initialize the buffer when the handler is added, fill it with data on receiving new bytes and start processing it when we get enough data.

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(); } } }

Comme nous pouvons le voir, il existe de nombreux détails en commun avec l'amorçage du serveur.

Nous pouvons maintenant exécuter la méthode principale du client et jeter un œil à la sortie de la console. Comme prévu, nous avons obtenu ResponseData avec intValue égal à 246.

5. Conclusion

Dans cet article, nous avons eu une introduction rapide à Netty. Nous avons montré ses composants de base tels que Channel et ChannelHandler . De plus, nous avons créé un simple serveur de protocole non bloquant et un client pour celui-ci.

Comme toujours, tous les exemples de code sont disponibles à l'adresse over sur GitHub.