Introduction à Netty

Introduction à Netty

1. introduction

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

L'objectif principal de Netty est de construire des serveurs de protocole hautes performances basés sur NIO (ou éventuellement NIO.2) avec séparation et couplage lâche des composants réseau et de la logique applicative. 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 entrées / sorties. Understanding non-blocking IO is crucial to understanding Netty’s core components and their relationships.

2.1. Canal

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

2.2. Futur

Chaque opération IO sur unChannel dans Netty est non bloquante.

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

C'est pourquoiNetty has its own ChannelFuture interface. 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. Le pipeline de traitement des données est donc une chaîne d'événements passant par des gestionnaires. Les événements et les gestionnaires peuvent être liés aux flux de données entrants et sortants. Les événements entrants peuvent être les suivants:

  • Activation et désactivation du canal

  • Lire les événements d'opération

  • Événements d'exception

  • Événements utilisateur

Les événements sortants sont plus simples et concernent généralement l'ouverture / la fermeture d'une connexion et l'écriture / le vidage des données.

Les applications Netty se composent de deux événements logiques de mise en réseau et d'application et de leurs gestionnaires. Les interfaces de base pour les gestionnaires d'événements de canal sontChannelHandler et ses ancêtresChannelOutboundHandler etChannelInboundHandler.

Netty fournit une énorme hiérarchie d'implémentations deChannelHandler. Il convient de noter les adaptateurs qui ne sont que des implémentations vides, par exemple ChannelInboundHandlerAdapter etChannelOutboundHandlerAdapter. Nous pourrions étendre ces adaptateurs lorsque nous n'avons besoin de traiter qu'un sous-ensemble de tous les événements.

De plus, il existe de nombreuses implémentations de protocoles spécifiques tels que HTTP, par exemple. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Il serait bon de se familiariser avec eux dans le Javadoc de Netty.

2.4. Encodeurs et décodeurs

Lorsque nous travaillons avec le protocole réseau, nous devons procéder à la sérialisation et à la désérialisation des données. Pour cela, Netty introduit des extensions spéciales desChannelInboundHandler pourdecoders qui sont capables de décoder les données entrantes. La classe de base de la plupart des décodeurs estByteToMessageDecoder.

Pour l'encodage des données sortantes, Netty a des extensions duChannelOutboundHandler appeléesencoders.MessageToByteEncoder est la base de la plupart des implémentations d'encodeurs. Nous pouvons convertir le message de la séquence d'octets en objet Java et vice versa avec les encodeurs et les 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. Les dépendances

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


    io.netty
    netty-all
    4.1.10.Final

Nous pouvons trouver la dernière version suron 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 demande et renvoie lesintValue multipliés 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.

Il convient de noter queNetty works with socket receive buffer, qui est représenté non pas comme une file d'attente mais simplement comme un ensemble d'octets. Cela signifie que notre gestionnaire entrant peut être appelé lorsque le message complet n'est pas reçu par un serveur.

We must make sure that we have received the full message before processing et il existe de nombreuses façons de le faire.

Tout d'abord, nous pouvons créer unByteBuf temporaire et y ajouter tous les octets entrants jusqu'à ce que nous obtenions la quantité d'octets requise:

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

L'exemple ci-dessus semble un peu bizarre mais nous aide à comprendre le fonctionnement de Netty. Chaque méthode de notre gestionnaire est appelée lorsque l'événement correspondant se produit. Nous initialisons donc la mémoire tampon lorsque le gestionnaire est ajouté, remplissons-la avec les données à la réception de nouveaux octets et commençons à la traiter lorsque nous recevons suffisamment de données.

Nous n'avons délibérément pas utilisé unstringValue - un décodage d'une telle manière serait inutilement complexe. C’est pourquoi Netty fournit des classes de décodeur utiles qui sont des implémentations deChannelInboundHandler:ByteToMessageDecoder etReplayingDecoder.

Comme nous l'avons noté ci-dessus, nous pouvons créer un pipeline de traitement de canal avec Netty. Nous pouvons donc placer notre décodeur en tant que premier gestionnaire, suivi du gestionnaire de logique de traitement.

Le décodeur pour RequestData est montré ensuite:

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


Une idée de ce décodeur est assez simple. Il utilise une implémentation deByteBuf qui lève une exception lorsqu'il n'y a pas assez de données dans le tampon pour l'opération de lecture.

Lorsque l'exception est interceptée, la mémoire tampon est renvoyée au début et le décodeur attend une nouvelle partie des données. Le décodage s'arrête lorsque la listeout n'est pas vide après l'exécution dedecode.

3.4. Encodeur de réponse

Outre le décodage desRequestData, nous devons encoder le message. Cette opération est plus simple car nous disposons de toutes les données du message lorsque l'opération d'écriture a lieu.

Nous pouvons écrire des données dansChannel dans notre gestionnaire principal ou nous pouvons séparer la logique et créer un gestionnaire étendantMessageToByteEncoder qui capturera l'opération d'écritureResponseData:

public class ResponseDataEncoder
  extends MessageToByteEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx,
      ResponseData msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getIntValue());
    }
}

3.5. Traitement des demandes

Puisque nous avons effectué le décodage et l'encodage dans des gestionnaires séparés, nous devons changer nosProcessingHandler:

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. Bootstrap du serveur

Maintenant, mettons tout en place et exécutons notre serveur:

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

Les détails des classes utilisées dans l'exemple de démarrage du serveur ci-dessus peuvent être trouvés dans leur Javadoc. La partie la plus intéressante est cette ligne:

ch.pipeline().addLast(
  new RequestDecoder(),
  new ResponseDataEncoder(),
  new ProcessingHandler());

Nous définissons ici les gestionnaires entrants et sortants qui traiteront les demandes et les afficheront dans le bon ordre.

4. Application client

Le client doit effectuer un encodage et un décodage inversés, nous devons donc avoir unRequestDataEncoder etResponseDataDecoder:

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


De plus, nous devons définir unClientHandler qui enverra la requête et recevra la réponse du serveur:

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

Maintenant, démarrons le 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 constater, de nombreux détails sont communs avec l’amorçage du serveur.

Nous pouvons maintenant exécuter la méthode principale du client et examiner la sortie de la console. Comme prévu, nous avons obtenuResponseData avecintValue égal à 246.

5. Conclusion

Dans cet article, nous avons eu une brève introduction à Netty. Nous avons montré ses composants de base tels queChannel etChannelHandler. De plus, nous avons créé un simple serveur de protocole non bloquant et un client pour celui-ci.

Comme toujours, tous les échantillons de code sont disponiblesover on GitHub.