Introduktion till Netty

1. Introduktion

I den här artikeln ska vi ta en titt på Netty - en asynkron händelsestyrd nätverksapplikationsram.

Huvudsyftet med Netty är att bygga högpresterande protokollservrar baserade på NIO (eller möjligen NIO.2) med separering och lös koppling av nätverket och affärslogikkomponenter. Det kan implementera ett allmänt känt protokoll, till exempel HTTP, eller ditt eget specifika protokoll.

2. Kärnbegrepp

Netty är ett icke-blockerande ramverk. Detta leder till hög genomströmning jämfört med att blockera IO. Att förstå icke-blockerande IO är avgörande för att förstå Netts kärnkomponenter och deras relationer.

2.1. Kanal

Channel är basen för Java NIO. Det representerar en öppen anslutning som kan IO-operationer som läsning och skrivning.

2.2. Framtida

Varje IO-operation på en kanal i Netty blockeras inte.

Detta innebär att varje operation returneras omedelbart efter samtalet. Det finns ett framtida gränssnitt i standard Java-biblioteket, men det är inte bekvämt för Netty-syften - vi kan bara fråga framtiden om slutförandet av operationen eller att blockera den aktuella tråden tills operationen är klar.

Därför har Netty sitt eget ChannelFuture- gränssnitt . Vi kan skicka en återuppringning till ChannelFuture som kommer att anropas när operationen är klar.

2.3. Händelser och hanterare

Netty använder ett händelsedrivet applikationsparadigm, så databearbetningens pipeline är en kedja av händelser som går igenom hanterare. Händelser och hanterare kan relateras till det in- och utgående dataflödet. Inkommande händelser kan vara följande:

  • Kanalaktivering och avaktivering
  • Läs driftshändelser
  • Undantagshändelser
  • Användarhändelser

Utgående händelser är enklare och är i allmänhet relaterade till att öppna / stänga en anslutning och skriva / spola data.

Netty-applikationer består av ett par nätverks- och applikationslogiska händelser och deras hanterare. Basgränssnitten för kanalhändelsehanterarna är ChannelHandler och dess förfäder ChannelOutboundHandler och ChannelInboundHandler .

Netty ger en enorm hierarki av implementeringar av ChannelHandler. Det är värt att notera adaptrarna som bara är tomma implementeringar, t.ex. ChannelInboundHandlerAdapter och ChannelOutboundHandlerAdapter . Vi kan utöka dessa adaptrar när vi bara behöver bearbeta en delmängd av alla händelser.

Det finns också många implementeringar av specifika protokoll som HTTP, t.ex. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Det skulle vara bra att bekanta sig med dem i Netts Javadoc.

2.4. Kodare och avkodare

När vi arbetar med nätverksprotokollet måste vi utföra dataserialisering och deserialisering. För detta ändamål introducerar Netty specialtillägg av ChannelInboundHandler för avkodare som kan avkoda inkommande data. Basklassen för de flesta avkodare är ByteToMessageDecoder.

För kodning av utgående data har Netty tillägg av ChannelOutboundHandler som kallas kodare. MessageToByteEncoder är basen för de flesta kodarimplementeringar . Vi kan konvertera meddelandet från bytesekvens till Java-objekt och vice versa med kodare och avkodare.

3. Exempel på serverapplikation

Låt oss skapa ett projekt som representerar en enkel protokollserver som tar emot en begäran, utför en beräkning och skickar ett svar.

3.1. Beroenden

Först och främst måste vi tillhandahålla Netty-beroendet i vår pom.xml :

 io.netty netty-all 4.1.10.Final 

Vi hittar den senaste versionen på Maven Central.

3.2. Datamodell

Dataklassen för begäran skulle ha följande struktur:

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

Låt oss anta att servern tar emot begäran och returnerar intValue multiplicerat med 2. Svaret skulle ha det enda int-värdet:

public class ResponseData { private int intValue; // standard getters and setters }

3.3. Begär avkodare

Nu måste vi skapa kodare och avkodare för våra protokollmeddelanden.

Det bör noteras att Netty arbetar med socket-mottagningsbuffert , som inte representeras som en kö utan bara som en massa byte. Detta innebär att vår inkommande hanterare kan anropas när hela meddelandet inte tas emot av en server.

Vi måste se till att vi har fått hela meddelandet innan behandlingen och det finns många sätt att göra det.

Först och främst kan vi skapa en tillfällig ByteBuf och lägga till alla inkomna byte tills vi får den erforderliga mängden byte:

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

Exemplet som visas ovan ser lite konstigt ut men hjälper oss att förstå hur Netty fungerar. Varje metod för vår hanterare anropas när motsvarande händelse inträffar. Så vi initialiserar bufferten när hanteraren läggs till, fyller den med data för att ta emot nya byte och börjar bearbeta den när vi får tillräckligt med data.

Vi använde medvetet inte en stringValue - avkodning på ett sådant sätt skulle vara onödigt komplicerat. Därför tillhandahåller Netty användbara avkodarklasser som är implementeringar av ChannelInboundHandler : ByteToMessageDecoder och ReplayingDecoder.

Som vi nämnde ovan kan vi skapa en kanalbehandlingsrörledning med Netty. Så vi kan sätta vår avkodare som den första hanteraren och behandlingslogikhanteraren kan komma efter den.

Avkodaren för RequestData visas nästa:

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

En uppfattning om denna avkodare är ganska enkel. Den använder en implementering av ByteBuf som ger ett undantag när det inte finns tillräckligt med data i bufferten för läsoperationen.

När undantaget fångas rullas bufferten tillbaka till början och avkodaren väntar på en ny del av data. Avkodningen stoppas när ut- listan inte är tom efter avkodning .

3.4. Svarskodare

Förutom att avkoda RequestData måste vi koda meddelandet. Denna operation är enklare eftersom vi har fullständiga meddelandedata när skrivoperationen sker.

Vi kan skriva data till Channel i vår huvudhanterare eller så kan vi separera logiken och skapa en hanterare som utökar MessageToByteEncoder som kommer att fånga svaret ResponseData :

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

3.5. Be om behandling

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

Som vi kan se finns det många detaljer gemensamt med server bootstrapping.

Nu kan vi köra klientens huvudmetod och titta på konsolutgången. Som förväntat fick vi ResponseData med intValue lika med 246.

5. Sammanfattning

I den här artikeln hade vi en snabb introduktion till Netty. Vi visade dess kärnkomponenter som Channel och ChannelHandler . Vi har också skapat en enkel protokollserver som inte blockerar och en klient för den.

Som alltid är alla kodprover tillgängliga på GitHub.