Java >> Java tutorial >  >> Tag >> Netty

Introduktion til Netty

1. Introduktion

I denne artikel skal vi tage et kig på Netty – en asynkron begivenhedsdrevet netværksapplikationsramme.

Hovedformålet med Netty er at bygge højtydende protokolservere baseret på NIO (eller muligvis NIO.2) med adskillelse og løs kobling af netværket og forretningslogikkomponenterne. Det kan implementere en almindeligt kendt protokol, såsom HTTP, eller din egen specifikke protokol.

2. Kernebegreber

Netty er en ikke-blokerende ramme. Dette fører til høj gennemstrømning sammenlignet med blokering af IO. Forståelse af ikke-blokerende IO er afgørende for at forstå Nettys kernekomponenter og deres relationer.

2.1. Kanal

Kanal er grundlaget for Java NIO. Det repræsenterer en åben forbindelse, som er i stand til IO-operationer såsom læsning og skrivning.

2.2. Fremtid

Hver IO-handling på en Kanal i Netty er ikke-blokerende.

Det betyder, at hver handling returneres umiddelbart efter opkaldet. Der er en Fremtid grænseflade i standard Java-biblioteket, men det er ikke praktisk til Netty-formål - vi kan kun spørge Fremtiden om afslutningen af ​​operationen eller for at blokere den aktuelle tråd, indtil operationen er udført.

Det er derfor Netty har sin egen ChannelFuture grænseflade . Vi kan sende et tilbagekald til ChannelFuture som vil blive kaldt efter operationens afslutning.

2.3. Begivenheder og Handlere

Netty bruger et hændelsesdrevet applikationsparadigme, så pipelinen af ​​databehandlingen er en kæde af hændelser, der går gennem behandlere. Hændelser og behandlere kan relateres til det indgående og udgående dataflow. Indgående hændelser kan være følgende:

  • Kanalaktivering og -deaktivering
  • Læs operationsbegivenheder
  • Undtagelsesbegivenheder
  • Brugerhændelser

Udgående hændelser er enklere og er generelt relateret til åbning/lukning af en forbindelse og skrivning/udskylning af data.

Netty-applikationer består af et par netværks- og applikationslogiske hændelser og deres behandlere. Basisgrænsefladerne for kanalhændelseshandlerne er ChannelHandler og dets efterfølgere ChannelOutboundHandler og ChannelInboundHandler .

Netty giver et enormt hierarki af implementeringer af ChannelHandler. Det er værd at bemærke adapterne som blot er tomme implementeringer, f.eks. ChannelInboundHandlerAdapter og ChannelOutboundHandlerAdapter . Vi kunne udvide disse adaptere, når vi kun skal behandle en delmængde af alle hændelser.

Der er også mange implementeringer af specifikke protokoller såsom HTTP, f.eks. HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Det ville være godt at stifte bekendtskab med dem i Nettys Javadoc.

2.4. Indkodere og dekodere

Når vi arbejder med netværksprotokollen, skal vi udføre dataserialisering og deserialisering. Til dette formål introducerer Netty særlige udvidelser af ChannelInboundHandler for dekodere som er i stand til at afkode indgående data. Basisklassen for de fleste dekodere er ByteToMessageDecoder.

Til kodning af udgående data har Netty udvidelser af ChannelOutboundHandler kaldet encodere. MessageToByteEncoder er basis for de fleste indkoderimplementeringer. Vi kan konvertere beskeden fra bytesekvens til Java-objekt og omvendt med indkodere og dekodere.

3. Eksempel på serverapplikation

Lad os oprette et projekt, der repræsenterer en simpel protokolserver, som modtager en anmodning, udfører en beregning og sender et svar.

3.1. Afhængigheder

Først og fremmest skal vi angive Netty-afhængigheden i vores pom.xml :

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.10.Final</version>
</dependency>

Vi kan finde den seneste version på Maven Central.

3.2. Datamodel

Anmodningsdataklassen ville have følgende struktur:

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

Lad os antage, at serveren modtager anmodningen og returnerer intValue ganget med 2. Svaret ville have den enkelte int-værdi:

public class ResponseData {
    private int intValue;

    // standard getters and setters
}

3.3. Anmod om dekoder

Nu skal vi lave indkodere og dekodere til vores protokolmeddelelser.

Det skal bemærkes, at Netty fungerer med socket-modtagebuffer , som ikke er repræsenteret som en kø, men blot som en flok bytes. Det betyder, at vores indgående handler kan kaldes, når den fulde besked ikke modtages af en server.

Vi skal sikre, at vi har modtaget hele meddelelsen, før vi behandler og der er mange måder at gøre det på.

Først og fremmest kan vi oprette en midlertidig ByteBuf og føje alle indgående bytes til det, indtil vi får det nødvendige antal bytes:

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

Eksemplet ovenfor ser lidt mærkeligt ud, men hjælper os med at forstå, hvordan Netty fungerer. Hver metode af vores handler kaldes, når dens tilsvarende hændelse indtræffer. Så vi initialiserer bufferen, når behandleren tilføjes, fylder den med data om modtagelse af nye bytes og begynder at behandle den, når vi får nok data.

Vi brugte bevidst ikke en stringValue — afkodning på en sådan måde ville være unødvendigt kompleks. Det er derfor, Netty tilbyder nyttige dekoderklasser, som er implementeringer af ChannelInboundHandler :ByteToMessageDecoder og ReplayingDecoder.

Som vi bemærkede ovenfor, kan vi oprette en kanalbehandlingspipeline med Netty. Så vi kan sætte vores dekoder som den første handler, og den logiske behandler kan komme efter den.

Dekoderen for RequestData vises herefter:

public class RequestDecoder extends ReplayingDecoder<RequestData> {

    private final Charset charset = Charset.forName("UTF-8");

    @Override
    protected void decode(ChannelHandlerContext ctx, 
      ByteBuf in, List<Object> 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 idé om denne dekoder er ret enkel. Den bruger en implementering af ByteBuf som kaster en undtagelse, når der ikke er nok data i bufferen til læseoperationen.

Når undtagelsen er fanget, spoles bufferen tilbage til begyndelsen, og dekoderen venter på en ny del af data. Afkodningen stopper, når ud listen er ikke tom efter afkodning udførelse.

3.4. Response Encoder

Udover at afkode RequestData vi skal indkode beskeden. Denne operation er enklere, fordi vi har de fulde beskeddata, når skriveoperationen finder sted.

Vi kan skrive data til Kanal i vores hovedhandler, eller vi kan adskille logikken og oprette en handler, der udvider MessageToByteEncoder som vil fange skrive ResponseData operation:

public class ResponseDataEncoder 
  extends MessageToByteEncoder<ResponseData> {

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

3.5. Anmodningsbehandling

Da vi udførte afkodningen og kodningen i separate handlere, er vi nødt til at ændre vores 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

Lad os nu samle det hele og køre vores 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<SocketChannel>() {
                @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();
        }
    }
}

Detaljerne for de klasser, der bruges i ovenstående server bootstrap eksempel kan findes i deres Javadoc. Den mest interessante del er denne linje:

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

Her definerer vi indgående og udgående handlere, der behandler anmodninger og output i den rigtige rækkefølge.

4. Klientapplikation

Klienten skal udføre omvendt kodning og afkodning, så vi skal have en RequestDataEncoder og ResponseDataDecoder :

public class RequestDataEncoder 
  extends MessageToByteEncoder<RequestData> {

    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<ResponseData> {

    @Override
    protected void decode(ChannelHandlerContext ctx, 
      ByteBuf in, List<Object> out) throws Exception {
 
        ResponseData data = new ResponseData();
        data.setIntValue(in.readInt());
        out.add(data);
    }
}

Vi skal også definere en ClientHandler som sender anmodningen og modtager svaret fra serveren:

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

Lad os nu bootstrap klienten:

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<SocketChannel>() {
 
                @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, er der mange detaljer til fælles med serverens bootstrapping.

Nu kan vi køre klientens hovedmetode og tage et kig på konsoloutputtet. Som forventet fik vi ResponseData med intValue lig med 246.

5. Konklusion

I denne artikel fik vi en hurtig introduktion til Netty. Vi viste dens kernekomponenter såsom Kanal og ChannelHandler . Vi har også lavet en simpel ikke-blokerende protokolserver og en klient til den.

Som altid er alle kodeeksempler tilgængelige på GitHub.


No
Java tag