Introduksjon til Netty

1. Introduksjon

I denne artikkelen skal vi ta en titt på Netty - et asynkront hendelsesdrevet nettverksapplikasjonsrammeverk.

Hovedformålet med Netty er å bygge høyytelsesprotokollservere basert på NIO (eller muligens NIO.2) med separasjon og løs kobling av nettverk og forretningslogikkomponenter. Det kan implementere en allment kjent protokoll, for eksempel HTTP, eller din egen spesifikke protokoll.

2. Kjernekonsepter

Netty er et ikke-blokkerende rammeverk. Dette fører til høy gjennomstrømning sammenlignet med blokkering av IO. Å forstå ikke-blokkerende IO er avgjørende for å forstå Netts kjernekomponenter og deres forhold.

2.1. Kanal

Kanal er basen til Java NIO. Det representerer en åpen forbindelse som er i stand til IO-operasjoner som lesing og skriving.

2.2. Framtid

Hver IO-operasjon på en Kanal i Netty er ikke-blokkerende.

Dette betyr at hver operasjon returneres umiddelbart etter samtalen. Det er en Framtid grensesnitt i standard Java-bibliotek, men det er ikke praktisk for Netty-formål - vi kan bare spørre Framtid om fullføring av operasjonen eller for å blokkere den gjeldende tråden til operasjonen er ferdig.

Derfor Netty har sin egen ChannelFuture grensesnitt. Vi kan ringe tilbake til ChannelFuture som vil bli kalt når operasjonen er fullført.

2.3. Arrangementer og håndtere

Netty bruker et hendelsesdrevet applikasjonsparadigme, så rørledningen til databehandlingen er en kjede av hendelser som går gjennom håndterere. Hendelser og behandlere kan være relatert til inn- og utgående dataflyt. Innkommende hendelser kan være følgende:

  • Kanalaktivering og deaktivering
  • Les operasjonshendelser
  • Unntakshendelser
  • Brukerhendelser

Utgående hendelser er enklere og har generelt sammenheng med å åpne / lukke en forbindelse og skrive / skylle data.

Nettsøknader består av et par nettverks- og applikasjonslogiske hendelser og deres håndterere. Basisgrensesnittene for kanalhendelsesbehandlerne er ChannelHandler og dets forfedre ChannelOutboundHandler og ChannelInboundHandler.

Netty gir et stort hierarki av implementeringer av ChannelHandler. Det er verdt å merke seg adapterne som bare er tomme implementeringer, f.eks. ChannelInboundHandlerAdapter og ChannelOutboundHandlerAdapter. Vi kan utvide disse adapterne når vi bare trenger å behandle en delmengde av alle hendelser.

Dessuten er det mange implementeringer av spesifikke protokoller som HTTP, f.eks. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Det ville være bra å bli kjent med dem i Netty's Javadoc.

2.4. Kodere og dekodere

Når vi jobber med nettverksprotokollen, må vi utføre dataserialisering og deserialisering. For dette formålet introduserer Netty spesielle utvidelser av ChannelInboundHandler til dekodere som er i stand til å dekode innkommende data. Baseklassen til de fleste dekodere er ByteToMessageDecoder.

For koding av utgående data har Netty utvidelser av ChannelOutboundHandler kalt kodere. MessageToByteEncoder er basen for de fleste kodereimplementeringer. Vi kan konvertere meldingen fra bytesekvens til Java-objekt og omvendt med kodere og dekodere.

3. Eksempel på serverapplikasjon

La oss lage et prosjekt som representerer en enkel protokollserver som mottar en forespørsel, utfører en beregning og sender et svar.

3.1. Avhengigheter

Først og fremst må vi gi nettavhengighet i vår pom.xml:

 io.netty netty-all 4.1.10.Final 

Vi finner den nyeste versjonen på Maven Central.

3.2. Datamodell

Dataklassen for forespørsel vil ha følgende struktur:

offentlig klasse RequestData {private int intValue; privat streng strengverdi; // standard getters og setters}

La oss anta at serveren mottar forespørselen og returnerer intValue multiplisert med 2. Svaret vil ha en enkelt int-verdi:

offentlig klasse ResponseData {private int intValue; // standard getters og setters}

3.3. Be om dekoder

Nå må vi lage kodere og dekodere for protokollmeldingene våre.

Det er verdt å merke seg at Netty jobber med socket mottaksbuffer, som ikke er representert som en kø, men bare som en haug med byte. Dette betyr at vår innkommende behandler kan ringes når hele meldingen ikke mottas av en server.

Vi må sørge for at vi har mottatt hele meldingen før behandlingen og det er mange måter å gjøre det på.

Først og fremst kan vi lage en midlertidig ByteBuf og legg til alle innkommende byte til vi får den nødvendige mengden byte:

offentlig klasse SimpleProcessingHandler utvider ChannelInboundHandlerAdapter {private ByteBuf tmp; @ Override public void handlerAdded (ChannelHandlerContext ctx) {System.out.println ("Handler lagt til"); tmp = ctx.alloc (). buffer (4); } @ Override public void handlerRemoved (ChannelHandlerContext ctx) {System.out.println ("Handler fjernet"); tmp.release (); tmp = null; } @ Override public void channelRead (ChannelHandlerContext ctx, Object msg) {ByteBuf m = (ByteBuf) msg; tmp.writeBytes (m); m.utgivelse (); hvis (tmp.readableBytes ()> = 4) {// forespørsel om behandling RequestData requestData = ny RequestData (); requestData.setIntValue (tmp.readInt ()); ResponseData responseData = nye ResponseData (); responseData.setIntValue (requestData.getIntValue () * 2); ChannelFuture fremtid = ctx.writeAndFlush (responsData); future.addListener (ChannelFutureListener.CLOSE); }}}

Eksemplet vist ovenfor ser litt rart ut, men hjelper oss å forstå hvordan Netty fungerer. Hver metode for vår handler kalles når den tilsvarende hendelsen inntreffer. Så vi initialiserer bufferen når behandleren er lagt til, fyller den med data om mottak av nye byte og begynner å behandle den når vi får nok data.

Vi brukte bevisst ikke en strengverdi - dekoding på en slik måte ville være unødvendig komplisert. Derfor gir Netty nyttige dekoderklasser som er implementeringer av ChannelInboundHandler: ByteToMessageDecoder og ReplayingDecoder.

Som vi nevnte ovenfor, kan vi lage en kanalbehandlingsrørledning med Netty. Så vi kan sette dekoderen vår som den første behandleren, og behandlingslogikkbehandleren kan komme etter den.

Dekoderen for RequestData vises neste:

offentlig klasse RequestDecoder utvider ReplayingDecoder {private final Charset charset = Charset.forName ("UTF-8"); @ Override-beskyttet ugyldig dekodning (ChannelHandlerContext ctx, ByteBuf in, List out) kaster Unntak {RequestData data = nye RequestData (); data.setIntValue (in.readInt ()); int strLen = in.readInt (); data.setStringValue (in.readCharSequence (strLen, charset) .toString ()); out.add (data); }}

En ide om denne dekoderen er ganske enkel. Den bruker en implementering av ByteBuf som kaster et unntak når det ikke er nok data i bufferen for leseoperasjonen.

Når unntaket er fanget, spoles bufferen tilbake til begynnelsen, og dekoderen venter på en ny del av dataene. Avkodingen stopper når ute listen er ikke tom etter dekode henrettelse.

3.4. Svarkoder

Foruten dekoding av RequestData vi trenger å kode meldingen. Denne operasjonen er enklere fordi vi har alle meldingsdata når skriveoperasjonen skjer.

Vi kan skrive data til Kanal i vår hovedbehandler eller så kan vi skille logikken og opprette en behandler som utvides MessageToByteEncoder som vil fange skrivingen ResponseData operasjon:

offentlig klasse ResponseDataEncoder utvider MessageToByteEncoder {@Override beskyttet tomkodning (ChannelHandlerContext ctx, ResponseData msg, ByteBuf out) kaster Unntak {out.writeInt (msg.getIntValue ()); }}

3.5. Be om behandling

Siden vi utførte avkodingen og kodingen i separate håndtere, må vi endre vår ProcessingHandler:

offentlig klasse ProcessingHandler utvider ChannelInboundHandlerAdapter {@Override public void channelRead (ChannelHandlerContext ctx, Object msg) kaster Unntak {RequestData requestData = (RequestData) msg; ResponseData responseData = nye ResponseData (); responseData.setIntValue (requestData.getIntValue () * 2); ChannelFuture fremtid = ctx.writeAndFlush (responsData); future.addListener (ChannelFutureListener.CLOSE); System.out.println (requestData); }}

3.6. Server Bootstrap

La oss nå sette alt sammen og kjøre serveren vår:

offentlig klasse NettyServer {privat int port; // constructor public static void main (String [] args) kaster Unntak {int port = args.length> 0? Integer.parseInt (args [0]); : 8080; ny NettyServer (port) .run (); } public void run () kaster Unntak {EventLoopGroup bossGroup = ny NioEventLoopGroup (); EventLoopGroup workerGroup = ny NioEventLoopGroup (); prøv {ServerBootstrap b = ny ServerBootstrap (); b.group (bossGroup, workerGroup) .channel (NioServerSocketChannel.class) .childHandler (new ChannelInitializer () {@ Override public void initChannel (SocketChannel ch) kaster Unntak {ch.pipeline (). addLast (ny RequestDecoder (), ny ResponseDataEc (), ny ProcessingHandler ());}}). alternativ (ChannelOption.SO_BACKLOG, 128) .childOption (ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind (port) .sync (); f.channel (). closeFuture (). sync (); } til slutt {workerGroup.shutdownGracefully (); bossGroup.shutdownGracefully (); }}}

Detaljene for klassene som brukes i ovennevnte server bootstrap-eksempel, finner du i deres Javadoc. Den mest interessante delen er denne linjen:

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

Her definerer vi inn- og utgående håndtere som behandler forespørsler og utdata i riktig rekkefølge.

4. Kundesøknad

Klienten skal utføre omvendt koding og dekoding, så vi må ha en RequestDataEncoder og ResponseDataDecoder:

offentlig klasse RequestDataEncoder utvider MessageToByteEncoder {private final Charset charset = Charset.forName ("UTF-8"); @ Override beskyttet tomkodning (ChannelHandlerContext ctx, RequestData msg, ByteBuf out) kaster Unntak {out.writeInt (msg.getIntValue ()); out.writeInt (msg.getStringValue (). lengde ()); out.writeCharSequence (msg.getStringValue (), charset); }}
offentlig klasse ResponseDataDecoder utvider ReplayingDecoder {@Override-beskyttet ugyldig dekode (ChannelHandlerContext ctx, ByteBuf in, List out) kaster Unntak {ResponseData data = new ResponseData (); data.setIntValue (in.readInt ()); out.add (data); }}

Vi må også definere en ClientHandler som vil sende forespørselen og motta svaret fra serveren:

offentlig klasse ClientHandler utvider ChannelInboundHandlerAdapter {@ Override public void channelActive (ChannelHandlerContext ctx) kaster Unntak {RequestData msg = new RequestData (); msg.setIntValue (123); msg.setStringValue ("alt arbeid og ikke noe spill gjør jack til en kjedelig gutt"); ChannelFuture fremtid = ctx.writeAndFlush (msg); } @Override public void channelRead (ChannelHandlerContext ctx, Object msg) kaster Unntak {System.out.println ((ResponseData) msg); ctx.close (); }}

La oss nå starte klienten:

offentlig klasse NettyClient {public static void main (String [] args) kaster Unntak {String host = "localhost"; int port = 8080; EventLoopGroup workerGroup = ny NioEventLoopGroup (); prøv {Bootstrap b = new Bootstrap (); b.group (workerGroup); b.kanal (NioSocketChannel.class); b.option (ChannelOption.SO_KEEPALIVE, true); b.handler (ny ChannelInitializer () {@Override public void initChannel (SocketChannel ch) kaster Unntak {ch.pipeline (). addLast (ny RequestDataEncoder (), ny ResponseDataDecoder (), ny ClientHandler ());}}); ChannelFuture f = b.connect (vert, port) .sync (); f.channel (). closeFuture (). sync (); } til slutt {workerGroup.shutdownGracefully (); }}}

Som vi kan se, er det mange detaljer felles med server bootstrapping.

Nå kan vi kjøre klientens hovedmetode og se på konsollutgangen. Som forventet fikk vi ResponseData med intValue lik 246.

5. Konklusjon

I denne artikkelen hadde vi en rask introduksjon til Netty. Vi viste kjernekomponentene som Kanal og ChannelHandler. Vi har også laget en enkel ikke-blokkerende protokollserver og en klient for den.

Som alltid er alle kodeeksempler tilgjengelig på GitHub.