HTTP Server con Netty

1. Panoramica

In questo tutorial, implementeremo un semplice server con lettere maiuscole su HTTP con Netty , un framework asincrono che ci offre la flessibilità per sviluppare applicazioni di rete in Java.

2. Bootstrap del server

Prima di iniziare, dovremmo essere consapevoli dei concetti di base di Netty, come canale, gestore, codificatore e decodificatore.

Qui passeremo direttamente al bootstrap del server, che è per lo più lo stesso di un semplice server di protocollo:

public class HttpServer { private int port; private static Logger logger = LoggerFactory.getLogger(HttpServer.class); // constructor // main method, same as simple protocol server public void run() throws Exception { ... ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new HttpRequestDecoder()); p.addLast(new HttpResponseEncoder()); p.addLast(new CustomHttpServerHandler()); } }); ... } } 

Quindi, qui solo il childHandler differisce in base al protocollo che vogliamo implementare , che è HTTP per noi.

Stiamo aggiungendo tre gestori alla pipeline del server:

  1. HttpResponseEncoder di Netty - per la serializzazione
  2. HttpRequestDecoder di Netty - per la deserializzazione
  3. Il nostro CustomHttpServerHandler - per definire il comportamento del nostro server

Diamo un'occhiata in dettaglio all'ultimo gestore.

3. CustomHttpServerHandler

Il compito del nostro gestore personalizzato è elaborare i dati in entrata e inviare una risposta.

Analizziamolo per capirne il funzionamento.

3.1. Struttura dell'handler

CustomHttpServerHandler estende il SimpleChannelInboundHandler astratto di Netty e implementa i suoi metodi del ciclo di vita:

public class CustomHttpServerHandler extends SimpleChannelInboundHandler { private HttpRequest request; StringBuilder responseData = new StringBuilder(); @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) { // implementation to follow } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }

Come suggerisce il nome del metodo, channelReadComplete svuota il contesto del gestore dopo che l'ultimo messaggio nel canale è stato utilizzato in modo che sia disponibile per il successivo messaggio in arrivo. Il metodo exceptionCaught serve per gestire le eventuali eccezioni.

Finora, tutto ciò che abbiamo visto è il codice boilerplate.

Ora andiamo avanti con le cose interessanti, l'implementazione di channelRead0 .

3.2. Leggere il canale

Il nostro caso d'uso è semplice, il server trasformerà semplicemente il corpo della richiesta e i parametri della query, se presenti, in maiuscolo. Una parola di cautela qui sul riflettere i dati della richiesta nella risposta: lo facciamo solo a scopo dimostrativo, per capire come possiamo usare Netty per implementare un server HTTP.

Qui, consumeremo il messaggio o la richiesta e imposteremo la sua risposta come raccomandato dal protocollo (nota che RequestUtils è qualcosa che scriveremo tra un momento):

if (msg instanceof HttpRequest) { HttpRequest request = this.request = (HttpRequest) msg; if (HttpUtil.is100ContinueExpected(request)) { writeResponse(ctx); } responseData.setLength(0); responseData.append(RequestUtils.formatParams(request)); } responseData.append(RequestUtils.evaluateDecoderResult(request)); if (msg instanceof HttpContent) { HttpContent httpContent = (HttpContent) msg; responseData.append(RequestUtils.formatBody(httpContent)); responseData.append(RequestUtils.evaluateDecoderResult(request)); if (msg instanceof LastHttpContent) { LastHttpContent trailer = (LastHttpContent) msg; responseData.append(RequestUtils.prepareLastResponse(request, trailer)); writeResponse(ctx, trailer, responseData); } } 

Come possiamo vedere, quando il nostro canale riceve un HttpRequest , controlla prima se la richiesta prevede uno stato di 100 Continue. In tal caso, rispondiamo immediatamente con una risposta vuota con uno stato CONTINUA :

private void writeResponse(ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); ctx.write(response); }

Dopodiché, il gestore inizializza una stringa da inviare come risposta e aggiunge ad essa i parametri di query della richiesta da restituire così com'è.

Definiamo ora il metodo formatParams e inseriamolo in una classe helper RequestUtils per farlo:

StringBuilder formatParams(HttpRequest request) { StringBuilder responseData = new StringBuilder(); QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); Map
      
        params = queryStringDecoder.parameters(); if (!params.isEmpty()) { for (Entry
       
         p : params.entrySet()) { String key = p.getKey(); List vals = p.getValue(); for (String val : vals) { responseData.append("Parameter: ").append(key.toUpperCase()).append(" = ") .append(val.toUpperCase()).append("\r\n"); } } responseData.append("\r\n"); } return responseData; }
       
      

Successivamente, alla ricezione di un HttpContent , prendiamo il corpo della richiesta e lo convertiamo in maiuscolo :

StringBuilder formatBody(HttpContent httpContent) { StringBuilder responseData = new StringBuilder(); ByteBuf content = httpContent.content(); if (content.isReadable()) { responseData.append(content.toString(CharsetUtil.UTF_8).toUpperCase()) .append("\r\n"); } return responseData; }

Inoltre, se l' HttpContent ricevuto è un LastHttpContent , aggiungiamo un messaggio di arrivederci e le intestazioni finali, se presenti:

StringBuilder prepareLastResponse(HttpRequest request, LastHttpContent trailer) { StringBuilder responseData = new StringBuilder(); responseData.append("Good Bye!\r\n"); if (!trailer.trailingHeaders().isEmpty()) { responseData.append("\r\n"); for (CharSequence name : trailer.trailingHeaders().names()) { for (CharSequence value : trailer.trailingHeaders().getAll(name)) { responseData.append("P.S. Trailing Header: "); responseData.append(name).append(" = ").append(value).append("\r\n"); } } responseData.append("\r\n"); } return responseData; }

3.3. Scrivere la risposta

Ora che i nostri dati da inviare sono pronti, possiamo scrivere la risposta al ChannelHandlerContext :

private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) { boolean keepAlive = HttpUtil.isKeepAlive(request); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, ((HttpObject) trailer).decoderResult().isSuccess() ? OK : BAD_REQUEST, Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8)); httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); if (keepAlive) { httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write(httpResponse); if (!keepAlive) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } }

In questo metodo, abbiamo creato una FullHttpResponse con la versione HTTP / 1.1, aggiungendo i dati che avevamo preparato in precedenza.

Se una richiesta deve essere mantenuta attiva, o in altre parole, se la connessione non deve essere chiusa, impostiamo l' intestazione della connessione della risposta come keep-alive . Altrimenti chiudiamo la connessione.

4. Verifica del server

Per testare il nostro server, inviamo alcuni comandi cURL e guardiamo le risposte.

Ovviamente, dobbiamo avviare il server eseguendo prima la classe HttpServer .

4.1. OTTIENI richiesta

Richiamiamo prima il server, fornendo un cookie con la richiesta:

curl //127.0.0.1:8080?param1=one

In risposta, otteniamo:

Parameter: PARAM1 = ONE Good Bye! 

Possiamo anche premere //127.0.0.1:8080?param1=one da qualsiasi browser per vedere lo stesso risultato.

4.2. Richiesta POST

Come secondo test, inviamo un POST con il contenuto del campione del corpo :

curl -d "sample content" -X POST //127.0.0.1:8080

Ecco la risposta:

SAMPLE CONTENT Good Bye!

Questa volta, poiché la nostra richiesta conteneva un corpo, il server lo ha rispedito in maiuscolo .

5. conclusione

In questo tutorial, abbiamo visto come implementare il protocollo HTTP, in particolare un server HTTP utilizzando Netty.

HTTP / 2 in Netty dimostra un'implementazione client-server del protocollo HTTP / 2.

Come sempre, il codice sorgente è disponibile su GitHub.