Introduzione a Netty

1. Introduzione

In questo articolo daremo uno sguardo a Netty, un framework per applicazioni di rete asincrono basato su eventi.

Lo scopo principale di Netty è costruire server di protocollo ad alte prestazioni basati su NIO (o forse NIO.2) con separazione e accoppiamento libero della rete e dei componenti della logica di business. Potrebbe implementare un protocollo ampiamente noto, come HTTP, o il tuo protocollo specifico.

2. Concetti fondamentali

Netty è un framework non bloccante. Ciò porta a un throughput elevato rispetto al blocco dell'I / O. Comprendere l'IO non bloccante è fondamentale per comprendere i componenti principali di Netty e le loro relazioni.

2.1. Canale

Channel è la base di Java NIO. Rappresenta una connessione aperta capace di operazioni di I / O come lettura e scrittura.

2.2. Futuro

Ogni operazione di I / O su un canale in Netty non è bloccante.

Ciò significa che ogni operazione viene restituita immediatamente dopo la chiamata. C'è un'interfaccia Future nella libreria Java standard, ma non è conveniente per gli scopi di Netty: possiamo solo chiedere al Future il completamento dell'operazione o bloccare il thread corrente fino al termine dell'operazione.

Ecco perché Netty ha la sua interfaccia ChannelFuture . Possiamo passare un callback a ChannelFuture che verrà chiamato al termine dell'operazione.

2.3. Eventi e gestori

Netty utilizza un paradigma di applicazione basato sugli eventi, quindi la pipeline dell'elaborazione dei dati è una catena di eventi che passa attraverso i gestori. Gli eventi e i gestori possono essere correlati al flusso di dati in entrata e in uscita. Gli eventi in entrata possono essere i seguenti:

  • Attivazione e disattivazione dei canali
  • Leggere gli eventi operativi
  • Eventi di eccezione
  • Eventi utente

Gli eventi in uscita sono più semplici e, generalmente, sono correlati all'apertura / chiusura di una connessione e alla scrittura / eliminazione dei dati.

Le applicazioni Netty sono costituite da un paio di eventi di rete e di logica dell'applicazione e dai relativi gestori. Le interfacce di base per i gestori di eventi di canale sono ChannelHandler e i suoi predecessori ChannelOutboundHandler e ChannelInboundHandler .

Netty fornisce un'enorme gerarchia di implementazioni di ChannelHandler. Vale la pena notare gli adattatori che sono solo implementazioni vuote, ad esempio ChannelInboundHandlerAdapter e ChannelOutboundHandlerAdapter . Potremmo estendere questi adattatori quando dobbiamo elaborare solo un sottoinsieme di tutti gli eventi.

Inoltre, ci sono molte implementazioni di protocolli specifici come HTTP, ad esempio HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Sarebbe bello conoscerli nel Javadoc di Netty.

2.4. Codificatori e decodificatori

Poiché lavoriamo con il protocollo di rete, è necessario eseguire la serializzazione e la deserializzazione dei dati. A tal fine, Netty introduce estensioni speciali di ChannelInboundHandler per i decoder in grado di decodificare i dati in arrivo. La classe base della maggior parte dei decoder è ByteToMessageDecoder.

Per la codifica dei dati in uscita, Netty dispone di estensioni di ChannelOutboundHandler chiamate encoder. MessageToByteEncoder è la base per la maggior parte delle implementazioni del codificatore . Possiamo convertire il messaggio dalla sequenza di byte all'oggetto Java e viceversa con codificatori e decodificatori.

3. Esempio di applicazione server

Creiamo un progetto che rappresenti un semplice protocollo server che riceve una richiesta, esegue un calcolo e invia una risposta.

3.1. Dipendenze

Prima di tutto, dobbiamo fornire la dipendenza Netty nel nostro pom.xml :

 io.netty netty-all 4.1.10.Final 

Possiamo trovare l'ultima versione su Maven Central.

3.2. Modello di dati

La classe di dati della richiesta avrebbe la seguente struttura:

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

Supponiamo che il server riceva la richiesta e restituisca intValue moltiplicato per 2. La risposta avrebbe il singolo valore int:

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

3.3. Richiedi Decoder

Ora dobbiamo creare codificatori e decodificatori per i nostri messaggi di protocollo.

Va notato che Netty funziona con il buffer di ricezione del socket , che è rappresentato non come una coda ma solo come un mucchio di byte. Ciò significa che il nostro gestore in entrata può essere chiamato quando il messaggio completo non viene ricevuto da un server.

Dobbiamo assicurarci di aver ricevuto il messaggio completo prima dell'elaborazione e ci sono molti modi per farlo.

Prima di tutto, possiamo creare un ByteBuf temporaneo e aggiungervi tutti i byte in entrata fino a ottenere la quantità di byte richiesta:

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

L'esempio mostrato sopra sembra un po 'strano ma ci aiuta a capire come funziona Netty. Ogni metodo del nostro gestore viene chiamato quando si verifica il suo evento corrispondente. Quindi inizializziamo il buffer quando viene aggiunto il gestore, lo riempiamo di dati alla ricezione di nuovi byte e iniziamo a elaborarlo quando otteniamo dati sufficienti.

Non abbiamo deliberatamente utilizzato stringValue - decodificare in questo modo sarebbe inutilmente complesso. Ecco perché Netty fornisce utili classi di decodificatore che sono implementazioni di ChannelInboundHandler : ByteToMessageDecoder e ReplayingDecoder .

Come abbiamo notato sopra, possiamo creare una pipeline di elaborazione del canale con Netty. Quindi possiamo mettere il nostro decoder come primo gestore e il gestore della logica di elaborazione può seguirlo.

Di seguito viene mostrato il decodificatore per RequestData:

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

Un'idea di questo decoder è piuttosto semplice. Utilizza un'implementazione di ByteBuf che genera un'eccezione quando non ci sono dati sufficienti nel buffer per l'operazione di lettura.

Quando l'eccezione viene rilevata, il buffer viene riavvolto all'inizio e il decodificatore attende una nuova porzione di dati. La decodifica si interrompe quando l' elenco in uscita non è vuoto dopo l' esecuzione della decodifica .

3.4. Encoder di risposta

Oltre a decodificare RequestData, dobbiamo codificare il messaggio. Questa operazione è più semplice perché abbiamo i dati completi del messaggio quando si verifica l'operazione di scrittura.

Possiamo scrivere dati su Channel nel nostro gestore principale oppure possiamo separare la logica e creare un gestore che estende MessageToByteEncoder che catturerà l' operazione di scrittura ResponseData :

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

3.5. Richiedi elaborazione

Poiché abbiamo eseguito la decodifica e la codifica in gestori separati, dobbiamo modificare il nostro 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. Bootstrap del server

Ora mettiamo tutto insieme ed eseguiamo il nostro 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(); } } }

I dettagli delle classi utilizzate nell'esempio di bootstrap del server sopra possono essere trovati nel loro Javadoc. La parte più interessante è questa linea:

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

Qui definiamo gestori in entrata e in uscita che elaboreranno le richieste e l'output nell'ordine corretto.

4. Applicazione client

Il client deve eseguire la codifica e la decodifica inversa, quindi è necessario disporre di RequestDataEncoder e 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); } }

Inoltre, dobbiamo definire un ClientHandler che invierà la richiesta e riceverà la risposta dal 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(); } }

Ora eseguiamo il bootstrap del 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(); } } }

Come possiamo vedere, ci sono molti dettagli in comune con il bootstrap del server.

Ora possiamo eseguire il metodo principale del client e dare un'occhiata all'output della console. Come previsto, abbiamo ottenuto ResponseData con intValue uguale a 246.

5. conclusione

In questo articolo, abbiamo avuto una rapida introduzione a Netty. Abbiamo mostrato i suoi componenti principali come Channel e ChannelHandler . Inoltre, abbiamo creato un semplice server di protocollo non bloccante e un client per questo.

Come sempre, tutti gli esempi di codice sono disponibili su GitHub.