Introductie tot Netty

1. Inleiding

In dit artikel gaan we Netty bekijken - een asynchroon, gebeurtenisgestuurd netwerkapplicatieframework.

Het belangrijkste doel van Netty is het bouwen van krachtige protocolservers op basis van NIO (of mogelijk NIO.2) met scheiding en losse koppeling van de netwerk- en bedrijfslogica-componenten. Het kan een algemeen bekend protocol implementeren, zoals HTTP, of uw eigen specifieke protocol.

2. Kernconcepten

Netty is een niet-blokkerend raamwerk. Dit leidt tot een hoge doorvoer in vergelijking met het blokkeren van IO. Het begrijpen van niet-blokkerende IO is cruciaal om de kerncomponenten van Netty en hun relaties te begrijpen.

2.1. Kanaal

Kanaal is de basis van Java NIO. Het vertegenwoordigt een open verbinding die in staat is tot IO-bewerkingen zoals lezen en schrijven.

2.2. Toekomst

Elke IO-bewerking op een Kanaal in Netty blokkeert niet.

Dit betekent dat elke bewerking onmiddellijk na de oproep wordt geretourneerd. Er is een Toekomst interface in de standaard Java-bibliotheek, maar het is niet handig voor Netty-doeleinden - we kunnen alleen de Toekomst over de voltooiing van de bewerking of om de huidige thread te blokkeren totdat de bewerking is voltooid.

Daarom Netty heeft zijn eigen ChannelFuture koppel. We kunnen terugbellen naar ChannelFuture die zal worden aangeroepen bij voltooiing van de operatie.

2.3. Evenementen en afhandelaars

Netty gebruikt een gebeurtenisgestuurd toepassingsparadigma, dus de pijplijn van de gegevensverwerking is een reeks gebeurtenissen die door handlers gaan. Gebeurtenissen en handlers kunnen worden gerelateerd aan de inkomende en uitgaande gegevensstroom. Inkomende evenementen kunnen de volgende zijn:

  • Kanaal activeren en deactiveren
  • Lees bewerkingsgebeurtenissen
  • Uitzonderingsgebeurtenissen
  • Gebruikersgebeurtenissen

Uitgaande gebeurtenissen zijn eenvoudiger en hebben over het algemeen betrekking op het openen / sluiten van een verbinding en het schrijven / wissen van gegevens.

Netty-applicaties bestaan ​​uit een aantal netwerk- en applicatielogische gebeurtenissen en hun handlers. De basisinterfaces voor de kanaalgebeurtenishandlers zijn ChannelHandler en zijn voorouders ChannelOutboundHandler en ChannelInboundHandler.

Netty biedt een enorme hiërarchie van implementaties van ChannelHandler. Het is vermeldenswaard dat de adapters slechts lege implementaties zijn, bijv. ChannelInboundHandlerAdapter en ChannelOutboundHandlerAdapter. We zouden deze adapters kunnen uitbreiden als we slechts een subset van alle gebeurtenissen hoeven te verwerken.

Er zijn ook veel implementaties van specifieke protocollen zoals HTTP, b.v. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Het zou goed zijn om met hen kennis te maken in Netty's Javadoc.

2.4. Encoders en decoders

Omdat we met het netwerkprotocol werken, moeten we dataserialisatie en deserialisatie uitvoeren. Voor dit doel introduceert Netty speciale uitbreidingen van de ChannelInboundHandler voor decoders die in staat zijn om inkomende gegevens te decoderen. De basisklasse van de meeste decoders is ByteToMessageDecoder.

Voor het coderen van uitgaande gegevens heeft Netty extensies van de ChannelOutboundHandler gebeld encoders. MessageToByteEncoder is de basis voor de meeste encoderimplementaties. We kunnen het bericht converteren van bytesequentie naar Java-object en vice versa met encoders en decoders.

3. Voorbeeld servertoepassing

Laten we een project maken dat een eenvoudige protocolserver vertegenwoordigt die een verzoek ontvangt, een berekening uitvoert en een antwoord verzendt.

3.1. Afhankelijkheden

Allereerst moeten we zorgen voor de Netty-afhankelijkheid in onze pom.xml:

 io.netty netty-all 4.1.10.Finale 

We kunnen de nieuwste versie vinden op Maven Central.

3.2. Gegevensmodel

De verzoekgegevensklasse zou de volgende structuur hebben:

openbare klasse RequestData {private int intValue; private String stringValue; // standaard getters en setters}

Laten we aannemen dat de server het verzoek ontvangt en de intValue vermenigvuldigd met 2. Het antwoord zou de enkele int-waarde hebben:

openbare klasse ResponseData {private int intValue; // standaard getters en setters}

3.3. Decoder aanvragen

Nu moeten we encoders en decoders maken voor onze protocolberichten.

het zou genoteerd moeten worden dat Netty werkt met een socket-ontvangstbuffer, die niet als een wachtrij wordt weergegeven, maar als een aantal bytes. Dit betekent dat onze inkomende handler kan worden gebeld wanneer het volledige bericht niet door een server is ontvangen.

We moeten ervoor zorgen dat we het volledige bericht hebben ontvangen voordat we het verwerken en er zijn veel manieren om dat te doen.

Allereerst kunnen we een tijdelijk ByteBuf en voeg alle inkomende bytes toe totdat we het vereiste aantal bytes hebben:

openbare klasse SimpleProcessingHandler breidt ChannelInboundHandlerAdapter {private ByteBuf tmp uit; @Override public void handlerAdded (ChannelHandlerContext ctx) {System.out.println ("Handler toegevoegd"); tmp = ctx.alloc (). buffer (4); } @Override public void handlerRemoved (ChannelHandlerContext ctx) {System.out.println ("Handler verwijderd"); 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) {// aanvraagverwerking RequestData requestData = nieuwe RequestData (); requestData.setIntValue (tmp.readInt ()); ResponseData responseData = nieuwe ResponseData (); responseData.setIntValue (requestData.getIntValue () * 2); ChannelFuture future = ctx.writeAndFlush (responseData); future.addListener (ChannelFutureListener.CLOSE); }}}

Het bovenstaande voorbeeld ziet er een beetje raar uit, maar helpt ons te begrijpen hoe Netty werkt. Elke methode van onze handler wordt aangeroepen wanneer de bijbehorende gebeurtenis plaatsvindt. Dus we initialiseren de buffer wanneer de handler wordt toegevoegd, vullen deze met gegevens bij het ontvangen van nieuwe bytes en beginnen deze te verwerken wanneer we voldoende gegevens hebben.

We hebben bewust geen gebruik gemaakt van een tekenreekswaarde - op een dergelijke manier decoderen zou onnodig ingewikkeld zijn. Daarom biedt Netty handige decoderklassen die implementaties zijn van ChannelInboundHandler: ByteToMessageDecoder en Herhalingsdecoder.

Zoals we hierboven hebben opgemerkt, kunnen we een kanaalverwerkingspijplijn maken met Netty. Dus we kunnen onze decoder als de eerste handler plaatsen en de verwerkingslogica kan erna komen.

De decoder voor RequestData wordt hierna getoond:

openbare klasse RequestDecoder breidt RepyingDecoder {private final Charset charset = Charset.forName ("UTF-8") uit; @Override beschermde ongeldige decodering (ChannelHandlerContext ctx, ByteBuf in, List out) genereert uitzondering {RequestData data = new RequestData (); data.setIntValue (in.readInt ()); int strLen = in.readInt (); data.setStringValue (in.readCharSequence (strLen, charset) .toString ()); out.add (data); }}

Een idee van deze decoder is vrij eenvoudig. Het maakt gebruik van een implementatie van ByteBuf die een uitzondering genereert wanneer er niet genoeg gegevens in de buffer zijn voor de leesbewerking.

Wanneer de uitzondering wordt opgevangen, wordt de buffer teruggespoeld naar het begin en wacht de decoder op een nieuw deel van de gegevens. Het decoderen stopt wanneer het uit lijst is niet leeg na decoderen executie.

3.4. Reactie-encoder

Naast het decoderen van de RequestData we moeten het bericht coderen. Deze bewerking is eenvoudiger omdat we de volledige berichtgegevens hebben wanneer de schrijfbewerking plaatsvindt.

We kunnen gegevens schrijven naar Kanaal in onze hoofdhandler of we kunnen de logica scheiden en een handleruitbreiding maken MessageToByteEncoder die het schrijven zal opvangen ResponseData operatie:

openbare klasse ResponseDataEncoder breidt MessageToByteEncoder {@Override beschermde ongeldige codering uit (ChannelHandlerContext ctx, ResponseData msg, ByteBuf out) gooit Uitzondering {out.writeInt (msg.getIntValue ()); }}

3.5. Verzoek om verwerking

Omdat we de decodering en codering in afzonderlijke handlers hebben uitgevoerd, moeten we onze VerwerkingHandler:

public class ProcessingHandler breidt ChannelInboundHandlerAdapter uit {@Override public void channelRead (ChannelHandlerContext ctx, Object msg) genereert uitzondering {RequestData requestData = (RequestData) msg; ResponseData responseData = nieuwe ResponseData (); responseData.setIntValue (requestData.getIntValue () * 2); ChannelFuture future = ctx.writeAndFlush (responseData); future.addListener (ChannelFutureListener.CLOSE); System.out.println (requestData); }}

3.6. Server Bootstrap

Laten we het nu allemaal samenvoegen en onze server laten draaien:

openbare klasse NettyServer {privé int-poort; // constructor public static void main (String [] args) gooit Uitzondering {int port = args.length> 0? Integer.parseInt (args [0]); : 8080; nieuwe NettyServer (poort) .run (); } public void run () gooit uitzondering {EventLoopGroup bossGroup = nieuwe NioEventLoopGroup (); EventLoopGroup workerGroup = nieuwe NioEventLoopGroup (); probeer {ServerBootstrap b = nieuwe ServerBootstrap (); b.group (bossGroup, workerGroup) .channel (NioServerSocketChannel.class) .childHandler (new ChannelInitializer () {@Override public void initChannel (SocketChannel ch) gooit uitzondering {ch.pipeline (). addLast (nieuwe RequestDecoder (), nieuwe ResponseDataEncoder (), nieuwe ProcessingHandler ());}}). optie (ChannelOption.SO_BACKLOG, 128) .childOption (ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind (poort) .sync (); f.channel (). closeFuture (). sync (); } eindelijk {workerGroup.shutdownGracefully (); bossGroup.shutdownGracefully (); }}}

De details van de klassen die in het bovenstaande server bootstrap-voorbeeld worden gebruikt, zijn te vinden in hun Javadoc. Het meest interessante deel is deze regel:

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

Hier definiëren we inkomende en uitgaande handlers die verzoeken en uitvoer in de juiste volgorde verwerken.

4. Clienttoepassing

De client moet reverse codering en decodering uitvoeren, dus we hebben een RequestDataEncoder en ResponseDataDecoder:

openbare klasse RequestDataEncoder breidt MessageToByteEncoder {private final Charset charset = Charset.forName ("UTF-8") uit; @Override beschermde ongeldige codering (ChannelHandlerContext ctx, RequestData msg, ByteBuf out) gooit uitzondering {out.writeInt (msg.getIntValue ()); out.writeInt (msg.getStringValue (). length ()); out.writeCharSequence (msg.getStringValue (), karakterset); }}
openbare klasse ResponseDataDecoder breidt ReplayingDecoder {@Override beschermde ongeldige decodering uit (ChannelHandlerContext ctx, ByteBuf in, List out) genereert uitzondering {ResponseData data = new ResponseData (); data.setIntValue (in.readInt ()); out.add (data); }}

We moeten ook een ClientHandler die het verzoek zal verzenden en het antwoord van de server zal ontvangen:

openbare klasse ClientHandler breidt ChannelInboundHandlerAdapter uit {@Override public void channelActive (ChannelHandlerContext ctx) genereert uitzondering {RequestData msg = new RequestData (); msg.setIntValue (123); msg.setStringValue ("al het werk en niet spelen maakt Jack een saaie jongen"); ChannelFuture toekomst = ctx.writeAndFlush (msg); } @Override public void channelRead (ChannelHandlerContext ctx, Object msg) genereert uitzondering {System.out.println ((ResponseData) msg); ctx.close (); }}

Laten we nu de client bootstrap:

openbare klasse NettyClient {openbare statische leegte hoofd (String [] args) gooit Uitzondering {String host = "localhost"; int poort = 8080; EventLoopGroup workerGroup = nieuwe NioEventLoopGroup (); probeer {Bootstrap b = nieuwe Bootstrap (); b.group (workerGroup); b.channel (NioSocketChannel.class); b.option (ChannelOption.SO_KEEPALIVE, true); b.handler (nieuwe ChannelInitializer () {@Override public void initChannel (SocketChannel ch) gooit uitzondering {ch.pipeline (). addLast (nieuwe RequestDataEncoder (), nieuwe ResponseDataDecoder (), nieuwe ClientHandler ());}}); ChannelFuture f = b.connect (host, poort) .sync (); f.channel (). closeFuture (). sync (); } eindelijk {workerGroup.shutdownGracefully (); }}}

Zoals we kunnen zien, zijn er veel details die gemeen hebben met het opstarten van de server.

Nu kunnen we de hoofdmethode van de client uitvoeren en de console-uitvoer bekijken. Zoals verwacht hebben we ResponseData met intValue gelijk aan 246.

5. Conclusie

In dit artikel kregen we een korte inleiding tot Netty. We hebben de kerncomponenten laten zien, zoals Kanaal en ChannelHandler. We hebben ook een eenvoudige niet-blokkerende protocolserver en een client ervoor gemaakt.

Zoals altijd zijn alle codevoorbeelden beschikbaar op GitHub.