HTTP Server mit Netty

1. Übersicht

In diesem Tutorial werden wir mit Netty , einem asynchronen Framework, das uns die Flexibilität bietet, Netzwerkanwendungen in Java zu entwickeln, einen einfachen Server im oberen Fall über HTTP implementieren .

2. Server Bootstrapping

Bevor wir beginnen, sollten wir uns mit den grundlegenden Konzepten von Netty wie Kanal, Handler, Encoder und Decoder vertraut machen.

Hier springen wir direkt zum Bootstrapping des Servers, der größtenteils mit einem einfachen Protokollserver identisch ist:

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

Hier unterscheidet sich also nur der childHandler gemäß dem Protokoll, das wir implementieren möchten , das für uns HTTP ist.

Wir fügen der Pipeline des Servers drei Handler hinzu:

  1. Netty ist HttpResponseEncoder - für die Serialisierung
  2. Netty ist HttpRequestDecoder - für die Deserialisierung
  3. Unser eigener CustomHttpServerHandler - zum Definieren des Verhaltens unseres Servers

Schauen wir uns als nächstes den letzten Handler im Detail an.

3. CustomHttpServerHandler

Die Aufgabe unseres benutzerdefinierten Handlers besteht darin, eingehende Daten zu verarbeiten und eine Antwort zu senden.

Lassen Sie es uns aufschlüsseln, um seine Funktionsweise zu verstehen.

3.1. Struktur des Handlers

CustomHttpServerHandler erweitert Nettys abstrakten SimpleChannelInboundHandler und implementiert seine Lebenszyklusmethoden:

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

Wie der Methodenname andeutet, löscht channelReadComplete den Handlerkontext, nachdem die letzte Nachricht im Kanal verbraucht wurde, damit sie für die nächste eingehende Nachricht verfügbar ist. Die Methode exceptionCaught dient zur Behandlung von Ausnahmen, falls vorhanden.

Bisher haben wir nur den Boilerplate-Code gesehen.

Kommen wir nun zu den interessanten Dingen , der Implementierung von channelRead0 .

3.2. Kanal lesen

Unser Anwendungsfall ist einfach: Der Server wandelt den Anforderungshauptteil und gegebenenfalls die Abfrageparameter einfach in Großbuchstaben um. Hier ist Vorsicht geboten, wenn es darum geht, Anforderungsdaten in der Antwort wiederzugeben. Wir tun dies nur zu Demonstrationszwecken, um zu verstehen, wie wir Netty zur Implementierung eines HTTP-Servers verwenden können.

Hier werden wir die Nachricht oder Anfrage konsumieren und ihre Antwort wie vom Protokoll empfohlen einrichten (beachten Sie, dass RequestUtils etwas ist, das wir gleich schreiben werden):

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

Wie wir sehen können, prüft unser Kanal beim Empfang einer HttpRequest zunächst, ob die Anfrage den Status 100 Continue erwartet. In diesem Fall schreiben wir sofort mit einer leeren Antwort mit dem Status WEITER zurück :

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

Danach initialisiert der Handler eine Zeichenfolge, die als Antwort gesendet werden soll, und fügt die Abfrageparameter der Anforderung hinzu, um sie unverändert zurückzusenden.

Definieren wir nun die Methode formatParams und platzieren sie dazu in einer RequestUtils-Hilfsklasse :

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

Als nächstes wird auf Empfang eines HttpContent , nehmen wir den Wunsch Körper und wandeln es in Großbuchstaben :

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

Wenn der empfangene HttpContent ein LastHttpContent ist , fügen wir eine Abschiedsnachricht und gegebenenfalls nachfolgende Header hinzu:

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. Antwort schreiben

Nachdem unsere zu sendenden Daten bereit sind, können wir die Antwort in den ChannelHandlerContext schreiben :

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

Bei dieser Methode haben wir eine FullHttpResponse mit HTTP / 1.1-Version erstellt und die zuvor vorbereiteten Daten hinzugefügt.

Wenn eine Anfrage zu halten Alive ist, oder in anderen Worten, wenn die Verbindung nicht geschlossen werden soll, setzten wir die Antwort Verbindung Header als Keep-Alive . Andernfalls schließen wir die Verbindung.

4. Testen des Servers

Um unseren Server zu testen, senden wir einige cURL-Befehle und sehen uns die Antworten an.

Natürlich müssen wir den Server starten, indem wir vorher die Klasse HttpServer ausführen .

4.1. GET Anfrage

Rufen wir zuerst den Server auf und stellen ein Cookie mit der Anfrage bereit:

curl //127.0.0.1:8080?param1=one

Als Antwort erhalten wir:

Parameter: PARAM1 = ONE Good Bye! 

Wir können auch von jedem Browser aus auf //127.0.0.1:8080?param1=one klicken , um das gleiche Ergebnis zu sehen.

4.2. POST-Anfrage

Senden wir als zweiten Test einen POST mit Inhalt der Körperprobe :

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

Hier ist die Antwort:

SAMPLE CONTENT Good Bye!

Da unsere Anfrage diesmal einen Text enthielt, schickte der Server ihn in Großbuchstaben zurück .

5. Schlussfolgerung

In diesem Tutorial haben wir gesehen, wie das HTTP-Protokoll implementiert wird, insbesondere ein HTTP-Server mit Netty.

HTTP / 2 in Netty demonstriert eine Client-Server-Implementierung des HTTP / 2-Protokolls.

Wie immer ist der Quellcode über GitHub verfügbar.