Einführung in Netty

Einführung in Netty

1. Einführung

In diesem Artikel werfen wir einen Blick auf Netty - ein asynchrones ereignisgesteuertes Netzwerkanwendungsframework.

Der Hauptzweck von Netty ist der Aufbau von Hochleistungs-Protokollservern auf der Basis von NIO (oder möglicherweise NIO.2) mit Trennung und loser Kopplung der Netzwerk- und Geschäftslogikkomponenten. Es kann ein allgemein bekanntes Protokoll wie HTTP oder Ihr eigenes spezifisches Protokoll implementieren.

2. Kernkonzepte

Netty ist ein nicht blockierendes Framework. Dies führt zu einem hohen Durchsatz im Vergleich zum Blockieren von E / A. Understanding non-blocking IO is crucial to understanding Netty’s core components and their relationships.

2.1. Kanal

Channel ist die Basis von Java NIO. Es handelt sich um eine offene Verbindung, die E / A-Operationen wie Lesen und Schreiben ausführen kann.

2.2. Zukunft

Jede E / A-Operation auf einemChannel in Netty ist nicht blockierend.

Dies bedeutet, dass jede Operation unmittelbar nach dem Aufruf zurückgegeben wird. In der Standard-Java-Bibliothek gibt es eineFuture-Schnittstelle, die jedoch für Netty-Zwecke nicht geeignet ist. Wir können nur dieFuture nach dem Abschluss der Operation fragen oder den aktuellen Thread blockieren, bis die Operation abgeschlossen ist .

Aus diesem Grund könnenNetty has its own ChannelFuture interface. einen Rückruf anChannelFuture übergeben, der nach Abschluss der Operation aufgerufen wird.

2.3. Ereignisse und Handler

Netty verwendet ein ereignisgesteuertes Anwendungsparadigma, daher ist die Pipeline der Datenverarbeitung eine Kette von Ereignissen, die durch Handler laufen. Ereignisse und Handler können mit dem eingehenden und ausgehenden Datenfluss verknüpft werden. Eingehende Ereignisse können folgende sein:

  • Kanal Aktivierung und Deaktivierung

  • Betriebsereignisse lesen

  • Ausnahmeereignisse

  • Benutzerereignisse

Ausgehende Ereignisse sind einfacher und beziehen sich im Allgemeinen auf das Öffnen / Schließen einer Verbindung und das Schreiben / Löschen von Daten.

Netty-Anwendungen bestehen aus mehreren Netzwerk- und Anwendungslogikereignissen und ihren Handlern. Die Basisschnittstellen für die Kanalereignishandler sindChannelHandler und ihre VorfahrenChannelOutboundHandler undChannelInboundHandler.

Netty bietet eine große Hierarchie von Implementierungen vonChannelHandler.. Es ist erwähnenswert, dass es sich bei den Adaptern nur um leere Implementierungen handelt, z. ChannelInboundHandlerAdapter undChannelOutboundHandlerAdapter. Wir könnten diese Adapter erweitern, wenn wir nur eine Teilmenge aller Ereignisse verarbeiten müssen.

Es gibt auch viele Implementierungen spezifischer Protokolle wie HTTP, z. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Es wäre gut, sie in Nettys Javadoc kennenzulernen.

2.4. Encoder und Decoder

Da wir mit dem Netzwerkprotokoll arbeiten, müssen wir die Serialisierung und Deserialisierung der Daten durchführen. Zu diesem Zweck führt Netty spezielle Erweiterungen derChannelInboundHandler fürdecoders ein, mit denen eingehende Daten dekodiert werden können. Die Basisklasse der meisten Decoder istByteToMessageDecoder.

Für die Codierung ausgehender Daten verfügt Netty über Erweiterungen derChannelOutboundHandler, die alsencoders.MessageToByteEncoder bezeichnet werden. Dies ist die Basis für die meisten Encoder-Implementierungen.. Wir können die Nachricht von der Byte-Sequenz in das Java-Objekt konvertieren und umgekehrt mit Encodern und Decodern.

3. Beispiel für eine Serveranwendung

Erstellen wir ein Projekt, das einen einfachen Protokollserver darstellt, der eine Anforderung empfängt, eine Berechnung durchführt und eine Antwort sendet.

3.1. Abhängigkeiten

Zunächst müssen wir die Netty-Abhängigkeit in unserenpom.xml angeben:


    io.netty
    netty-all
    4.1.10.Final

Wir können die neueste Version überon Maven Central finden.

3.2. Datenmodell

Die Anforderungsdatenklasse hätte die folgende Struktur:

public class RequestData {
    private int intValue;
    private String stringValue;

    // standard getters and setters
}

Nehmen wir an, der Server empfängt die Anforderung und gibt dieintValue multipliziert mit 2 zurück. Die Antwort hätte den Wert single int:

public class ResponseData {
    private int intValue;

    // standard getters and setters
}

3.3. Decoder anfordern

Jetzt müssen wir Encoder und Decoder für unsere Protokollnachrichten erstellen.

Es ist zu beachten, dassNetty works with socket receive buffer nicht als Warteschlange, sondern nur als Bündel von Bytes dargestellt wird. Dies bedeutet, dass unser Inbound-Handler aufgerufen werden kann, wenn die vollständige Nachricht nicht von einem Server empfangen wird.

We must make sure that we have received the full message before processing und es gibt viele Möglichkeiten, dies zu tun.

Zunächst können wir ein temporäresByteBuf erstellen und alle eingehenden Bytes an dieses anhängen, bis wir die erforderliche Anzahl von Bytes erhalten:

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

Das oben gezeigte Beispiel sieht etwas seltsam aus, hilft uns jedoch zu verstehen, wie Netty funktioniert. Jede Methode unseres Handlers wird aufgerufen, wenn das entsprechende Ereignis eintritt. Wir initialisieren also den Puffer, wenn der Handler hinzugefügt wird, füllen ihn mit Daten beim Empfang neuer Bytes und beginnen mit der Verarbeitung, wenn wir genügend Daten erhalten.

Wir haben bewusst keinstringValue verwendet - eine solche Dekodierung wäre unnötig komplex. Aus diesem Grund bietet Netty nützliche Decoderklassen an, bei denen es sich um Implementierungen vonChannelInboundHandler handelt:ByteToMessageDecoder undReplayingDecoder.

Wie oben erwähnt, können wir mit Netty eine Kanalverarbeitungs-Pipeline erstellen. So können wir unseren Decoder als ersten Handler einsetzen, und der Verarbeitungslogik-Handler kann danach kommen.

Der Decoder für RequestData wird als nächstes angezeigt:

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


Eine Idee dieses Decoders ist ziemlich einfach. Es wird eine Implementierung vonByteBuf verwendet, die eine Ausnahme auslöst, wenn sich nicht genügend Daten im Puffer für die Leseoperation befinden.

Wenn die Ausnahme abgefangen wird, wird der Puffer an den Anfang zurückgespult und der Decoder wartet auf einen neuen Teil der Daten. Die Dekodierung wird beendet, wenn die Liste vonoutnach der Ausführung vondecodenicht leer ist.

3.4. Antwortcodierer

Neben der Dekodierung derRequestData müssen wir die Nachricht kodieren. Diese Operation ist einfacher, da wir die vollständigen Nachrichtendaten haben, wenn die Schreiboperation auftritt.

Wir können Daten inChannel in unserem Haupthandler schreiben oder wir können die Logik trennen und einen Handler erstellen, derMessageToByteEncoder erweitert und die SchreiboperationResponseDataabfängt:

public class ResponseDataEncoder
  extends MessageToByteEncoder {

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

3.5. Anforderungsverarbeitung

Da wir die Dekodierung und Kodierung in separaten Handlern durchgeführt haben, müssen wir unsereProcessingHandler ändern:

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

Lassen Sie uns nun alles zusammenfügen und unseren Server ausführen:

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

Die Details der im obigen Server-Bootstrap-Beispiel verwendeten Klassen finden Sie in ihrem Javadoc. Der interessanteste Teil ist diese Zeile:

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

Hier definieren wir Inbound- und Outbound-Handler, die Anforderungen verarbeiten und in der richtigen Reihenfolge ausgeben.

4. Client-Anwendung

Der Client sollte eine umgekehrte Codierung und Decodierung durchführen, daher benötigen wirRequestDataEncoder undResponseDataDecoder:

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


Außerdem müssen wirClientHandler definieren, die die Anforderung senden und die Antwort vom Server empfangen:

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

Lassen Sie uns nun den Client booten:

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

Wie wir sehen können, gibt es viele Gemeinsamkeiten mit dem Server-Bootstrapping.

Jetzt können wir die Hauptmethode des Clients ausführen und einen Blick auf die Konsolenausgabe werfen. Wie erwartet haben wirResponseData mitintValue gleich 246.

5. Fazit

In diesem Artikel hatten wir eine kurze Einführung in Netty. Wir haben seine Kernkomponenten wieChannel undChannelHandler gezeigt. Außerdem haben wir einen einfachen nicht blockierenden Protokollserver und einen Client dafür erstellt.

Wie immer sind alle Codebeispieleover on GitHub verfügbar.