Einführung in Netty

1. Einleitung

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

Der Hauptzweck von Netty ist der Aufbau von Hochleistungsprotokollservern auf Basis von NIO (oder möglicherweise NIO.2) mit Trennung und loser Kopplung der Netzwerk- und Geschäftslogikkomponenten. Möglicherweise wird ein weithin bekanntes Protokoll wie HTTP oder Ihr eigenes spezifisches Protokoll implementiert.

2. Kernkonzepte

Netty ist ein nicht blockierendes Framework. Dies führt zu einem hohen Durchsatz im Vergleich zum Blockieren von E / A. Das Verständnis nicht blockierender E / A ist entscheidend für das Verständnis der Kernkomponenten von Netty und ihrer Beziehungen.

2.1. Kanal

Channel ist die Basis von Java NIO. Es stellt eine offene Verbindung dar, die E / A-Vorgänge wie Lesen und Schreiben ausführen kann.

2.2. Zukunft

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

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

Aus diesem Grund verfügt Netty über eine eigene ChannelFuture- Oberfläche . Wir können einen Rückruf an ChannelFuture weiterleiten, der nach Abschluss der Operation aufgerufen wird.

2.3. Ereignisse und Handler

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

  • Kanalaktivierung 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 einigen Netzwerk- und Anwendungslogikereignissen und ihren Handlern. Die Basisschnittstellen für die Kanalereignishandler sind ChannelHandler und seine Vorfahren ChannelOutboundHandler und ChannelInboundHandler .

Netty bietet eine riesige Hierarchie von Implementierungen von ChannelHandler. Beachten Sie die Adapter, bei denen es sich nur um leere Implementierungen handelt, z . B. ChannelInboundHandlerAdapter und ChannelOutboundHandlerAdapter . 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. B. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Es wäre gut, sie in Nettys Javadoc kennenzulernen.

2.4. Encoder und Decoder

Während wir mit dem Netzwerkprotokoll arbeiten, müssen wir eine Serialisierung und Deserialisierung der Daten durchführen. Zu diesem Zweck führt Netty spezielle Erweiterungen des ChannelInboundHandler für Decoder ein, die eingehende Daten decodieren können. Die Basisklasse der meisten Decoder ist ByteToMessageDecoder.

Für die Codierung ausgehender Daten verfügt Netty über Erweiterungen des ChannelOutboundHandler, die als Encoder bezeichnet werden. MessageToByteEncoder ist die Basis für die meisten Encoder-Implementierungen . Wir können die Nachricht von der Bytesequenz in ein Java-Objekt konvertieren und umgekehrt mit Codierern und Decodierern.

3. Beispiel einer 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 unserer pom.xml angeben :

 io.netty netty-all 4.1.10.Final 

Wir finden die neueste Version auf Maven Central.

3.2. Datenmodell

Die Anforderungsdatenklasse hätte die folgende Struktur:

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

Angenommen , der Server empfängt die Anforderung und gibt den mit 2 multiplizierten intValue zurück . Die Antwort hätte den einzelnen int-Wert:

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, dass Netty mit dem Socket-Empfangspuffer arbeitet , der nicht als Warteschlange, sondern nur als Bündel von Bytes dargestellt wird. Dies bedeutet, dass unser eingehender Handler aufgerufen werden kann, wenn die vollständige Nachricht nicht von einem Server empfangen wird.

Wir müssen sicherstellen, dass wir die vollständige Nachricht erhalten haben, bevor wir sie verarbeiten, und es gibt viele Möglichkeiten, dies zu tun.

Zunächst können wir einen temporären ByteBuf erstellen und alle eingehenden Bytes an diesen 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 aber zu verstehen, wie Netty funktioniert. Jede Methode unseres Handlers wird aufgerufen, wenn das entsprechende Ereignis eintritt. Also initialisieren wir 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.

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

Wie wir sehen können, gibt es viele Details, die mit dem Server-Bootstrapping gemeinsam sind.

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

5. Schlussfolgerung

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

Wie immer sind alle Codebeispiele auf GitHub verfügbar.