HTTP-server met Netty

1. Overzicht

In deze tutorial gaan we dat doen implementeer een eenvoudige server in de bovenste behuizing HTTP met Netty, een asynchroon raamwerk dat ons de flexibiliteit geeft om netwerktoepassingen in Java te ontwikkelen.

2. Server bootstrapping

Voordat we beginnen, moeten we op de hoogte zijn van de basisconcepten van Netty, zoals kanaal, handler, encoder en decoder.

Hier gaan we meteen naar het bootstrappen van de server, wat grotendeels hetzelfde is als een eenvoudige protocolserver:

openbare klasse HttpServer {privé int-poort; privé statische Logger-logger = LoggerFactory.getLogger (HttpServer.class); // constructor // hoofdmethode, hetzelfde als een eenvoudig protocol server public void run () gooit uitzondering {... ServerBootstrap b = nieuwe ServerBootstrap (); b.group (bossGroup, workerGroup) .channel (NioServerSocketChannel.class) .handler (nieuwe LoggingHandler (LogLevel.INFO)) .childHandler (nieuwe ChannelInitializer () {@Override beschermde leegte initChannel (SocketChannel ch) gooit Uitzondering {ChannelPipeline peline = .pipeline (); p.addLast (nieuwe HttpRequestDecoder ()); p.addLast (nieuwe HttpResponseEncoder ()); p.addLast (nieuwe CustomHttpServerHandler ());}}); ...}} 

Dus hier alleen de childHandler verschilt volgens het protocol dat we willen implementeren, wat voor ons HTTP is.

We voegen drie handlers toe aan de pijplijn van de server:

  1. Netty's HttpResponseEncoder - voor serialisatie
  2. Netty's HttpRequestDecoder - voor deserialisatie
  3. Ons eigen CustomHttpServerHandler - voor het bepalen van het gedrag van onze server

Laten we hierna de laatste handler in detail bekijken.

3. CustomHttpServerHandler

Het is de taak van onze aangepaste handler om inkomende gegevens te verwerken en een reactie te verzenden.

Laten we het opsplitsen om de werking ervan te begrijpen.

3.1. Structuur van de handler

CustomHttpServerHandler breidt Netty's samenvatting uit SimpleChannelInboundHandler en implementeert zijn levenscyclusmethoden:

openbare klasse CustomHttpServerHandler breidt SimpleChannelInboundHandler {privé HttpRequest-verzoek uit; StringBuilder responseData = nieuwe StringBuilder (); @Override public void channelReadComplete (ChannelHandlerContext ctx) {ctx.flush (); } @Override protected void channelRead0 (ChannelHandlerContext ctx, Object msg) {// implementatie om te volgen} @Override public void exceptionCaught (ChannelHandlerContext ctx, Throwable oorzaak) {cause.printStackTrace (); ctx.close (); }}

Zoals de naam van de methode suggereert, channelReadComplete wist de handlercontext nadat het laatste bericht in het kanaal is verbruikt, zodat het beschikbaar is voor het volgende inkomende bericht. De methode exceptionCaught is voor het afhandelen van eventuele uitzonderingen.

Tot nu toe hebben we alleen de standaardcode gezien.

Laten we nu aan de slag gaan met de interessante dingen, de implementatie van channelRead0.

3.2. Het kanaal lezen

Ons gebruik is eenvoudig, de server zal de hoofdtekst van het verzoek en eventuele queryparameters eenvoudig omzetten in hoofdletters. Een woord van waarschuwing hier over het weergeven van verzoekgegevens in het antwoord - we doen dit alleen voor demonstratiedoeleinden, om te begrijpen hoe we Netty kunnen gebruiken om een ​​HTTP-server te implementeren.

Hier, we consumeren het bericht of verzoek en stellen het antwoord in zoals aanbevolen door het protocol (Let daar op RequestUtils is iets dat we in een ogenblik zullen schrijven):

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

Zoals we kunnen zien, ontvangt ons kanaal een HttpRequest, controleert het eerst of het verzoek een 100 Doorgaan-status verwacht. In dat geval schrijven we direct terug met een lege reactie met de status van DOORGAAN MET:

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

Daarna initialiseert de handler een string die als antwoord moet worden verzonden en voegt de queryparameters van het verzoek eraan toe om te worden teruggestuurd zoals het is.

Laten we nu de methode definiëren formatParams en plaats het in een RequestUtils helper class om dat te doen:

StringBuilder formatParams (HttpRequest-verzoek) {StringBuilder responseData = nieuwe StringBuilder (); QueryStringDecoder queryStringDecoder = nieuwe QueryStringDecoder (request.uri ()); Kaart params = queryStringDecoder.parameters (); if (! params.isEmpty ()) {voor (Entry p: params.entrySet ()) {String key = p.getKey (); Lijstwaarden = p.getValue (); voor (String val: vals) {responseData.append ("Parameter:") .append (key.toUpperCase ()). append ("=") .append (val.toUpperCase ()). append ("\ r \ n "); }} responseData.append ("\ r \ n"); } return responseData; }

Vervolgens, bij het ontvangen van een HttpContent, we nemen de hoofdtekst van het verzoek en zetten deze om in hoofdletters:

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

Ook als het ontvangen HttpContent is een LastHttpContent, voegen we een afscheidsbericht en eventuele koppen achteraan toe:

StringBuilder preparLastResponse (HttpRequest-verzoek, LastHttpContent-trailer) {StringBuilder responseData = nieuwe StringBuilder (); responseData.append ("Tot ziens! \ r \ n"); if (! trailer.trailingHeaders (). isEmpty ()) {responseData.append ("\ r \ n"); voor (CharSequence naam: trailer.trailingHeaders (). names ()) {voor (CharSequence waarde: trailer.trailingHeaders (). getAll (naam)) {responseData.append ("P.S. Trailing Header:"); responseData.append (naam) .append ("=") .append (waarde) .append ("\ r \ n"); }} responseData.append ("\ r \ n"); } return responseData; }

3.3. Het antwoord schrijven

Nu onze te verzenden gegevens gereed zijn, kunnen we het antwoord schrijven naar het ChannelHandlerContext:

private void writeResponse (ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) {boolean keepAlive = HttpUtil.isKeepAlive (verzoek); FullHttpResponse httpResponse = nieuw DefaultFullHttpResponse (HTTP_1_1, ((HttpObject) trailer) .decoderResult (). IsSuccess ()? OK: BAD_REQUEST, Unpooled.copiedBuffer (responseData.toString (), CharsetUtil). 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); }}

Bij deze methode hebben we een FullHttpResponse met HTTP / 1.1-versie, waarbij de gegevens worden toegevoegd die we eerder hadden voorbereid.

Als een verzoek in leven moet worden gehouden, of met andere woorden, als de verbinding niet moet worden gesloten, stellen we de respons in verbinding koptekst als in leven houden. Anders sluiten we de verbinding.

4. Testen van de server

Om onze server te testen, sturen we wat cURL-opdrachten en kijken we naar de antwoorden.

Natuurlijk, we moeten de server starten door de klasse uit te voeren HttpServer voor dit.

4.1. KRIJG Verzoek

Laten we eerst de server aanroepen en een cookie voorzien van het verzoek:

krul //127.0.0.1:8080?param1=one

Als reactie krijgen we:

Parameter: PARAM1 = EEN Tot ziens! 

We kunnen ook slaan //127.0.0.1:8080?param1=one vanuit elke browser om hetzelfde resultaat te zien.

4.2. POST-verzoek

Laten we als onze tweede test een POST met body sturen voorbeeld inhoud:

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

Hier is het antwoord:

VOORBEELDINHOUD Tot ziens!

Deze keer, aangezien ons verzoek een lichaam bevatte, de server heeft het in hoofdletters teruggestuurd.

5. Conclusie

In deze tutorial hebben we gezien hoe we het HTTP-protocol kunnen implementeren, met name een HTTP-server die Netty gebruikt.

HTTP / 2 in Netty demonstreert een client-server-implementatie van het HTTP / 2-protocol.

Zoals altijd is de broncode beschikbaar op GitHub.