HTTP-server med Netty

1. Oversigt

I denne vejledning skal vi implementere en simpel server med overkapper over HTTP med Netty, en asynkron ramme, der giver os fleksibiliteten til at udvikle netværksapplikationer i Java.

2. Server bootstrapping

Inden vi starter, skal vi være opmærksomme på de grundlæggende begreber i Netty, såsom kanal, handler, encoder og dekoder.

Her hopper vi lige ind i bootstrapping af serveren, som for det meste er den samme som en simpel protokolserver:

offentlig klasse HttpServer {privat int port; privat statisk loggerlogger = LoggerFactory.getLogger (HttpServer.class); // konstruktør // hovedmetode, samme som simpel protokolserver offentlig ugyldig kørsel () kaster undtagelse {... ServerBootstrap b = ny ServerBootstrap (); b.group (bossGroup, workerGroup) .channel (NioServerSocketChannel.class) .handler (new LoggingHandler (LogLevel.INFO)) .childHandler (new ChannelInitializer () {@Override protected void initChannel (SocketChannel ch) kaster undtagelse {ChannelPipeline p = ch .pipeline (); p.addLast (ny HttpRequestDecoder ()); p.addLast (ny HttpResponseEncoder ()); p.addLast (ny CustomHttpServerHandler ());}}); ...}} 

Så her kun den childHandler adskiller sig i henhold til den protokol, vi vil implementere, som er HTTP for os.

Vi tilføjer tre håndterere til serverens pipeline:

  1. Netty's HttpResponseEncoder - til serialisering
  2. Netty's HttpRequestDecoder - til deserialisering
  3. Vores egen CustomHttpServerHandler - til at definere vores servers adfærd

Lad os se på den sidste handler i det næste.

3. CustomHttpServerHandler

Vores brugerdefinerede handler har til opgave at behandle indgående data og sende et svar.

Lad os nedbryde det for at forstå, hvordan det fungerer.

3.1. Handlerens struktur

CustomHttpServerHandler udvider Netys abstrakt SimpleChannelInboundHandler og implementerer dets livscyklusmetoder:

offentlig klasse CustomHttpServerHandler udvider SimpleChannelInboundHandler {privat HttpRequest anmodning; StringBuilder responseData = ny StringBuilder (); @ Override offentlig ugyldig channelReadComplete (ChannelHandlerContext ctx) {ctx.flush (); } @ Override beskyttet ugyldigt channelRead0 (ChannelHandlerContext ctx, Object msg) {// implementering skal følges} @ Override public void exceptionCaught (ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace (); ctx.close (); }}

Som metodens navn antyder, channelReadComplete skyller handler-konteksten, efter at den sidste besked i kanalen er forbrugt, så den er tilgængelig til den næste indgående besked. Metoden undtagelse fanget er til håndtering af eventuelle undtagelser.

Indtil videre er alt, hvad vi har set, kogepladekoden.

Lad os nu komme videre med de interessante ting, implementeringen af channelRead0.

3.2. Læsning af kanalen

Vores brugssag er enkel, serveren transformerer simpelthen anmodningens brødtekst og forespørgselsparametre til store bogstaver. Et ord med forsigtighed her om at afspejle anmodningsdata i svaret - vi gør dette kun til demonstrationsformål for at forstå, hvordan vi kan bruge Netty til at implementere en HTTP-server.

Her, vi forbruger beskeden eller anmodningen og indstiller dens svar som anbefalet af protokollen (Noter det RequestUtils er noget, vi skriver om et øjeblik):

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

Som vi kan se, når vores kanal modtager en HttpForespørgsel, kontrollerer den først, om anmodningen forventer en 100 Fortsæt-status. I så fald skriver vi straks tilbage med et tomt svar med status på BLIVE VED:

privat ugyldigt writeResponse (ChannelHandlerContext ctx) {FullHttpResponse svar = nyt DefaultFullHttpResponse (HTTP_1_1, FORTSÆT, ikke poolet.EMPTY_BUFFER); ctx.write (svar); }

Derefter initialiserer handleren en streng, der skal sendes som et svar, og tilføjer anmodningens forespørgselsparametre til den, der skal sendes tilbage som den er.

Lad os nu definere metoden formatParams og placer den i en RequestUtils hjælperklasse til at gøre det:

StringBuilder formatParams (HttpRequest anmodning) {StringBuilder responseData = ny StringBuilder (); QueryStringDecoder queryStringDecoder = ny QueryStringDecoder (request.uri ()); Kort params = queryStringDecoder.parameters (); hvis (! params.isEmpty ()) {for (Entry p: params.entrySet ()) {Strengnøgle = p.getKey (); Liste vals = p.getValue (); for (String val: vals) {responsData.append ("Parameter:") .append (key.toUpperCase ()). append ("=") .append (val.toUpperCase ()). append ("\ r \ n "); }} responseData.append ("\ r \ n"); } returnere svarData; }

Dernæst ved modtagelse af en HttpIndhold, vi tager anmodningsorganet og konverterer det til store bogstaver:

StringBuilder formatBody (HttpContent httpContent) {StringBuilder responseData = ny StringBuilder (); ByteBuf-indhold = httpContent.content (); hvis (content.isReadable ()) {responsData.append (content.toString (CharsetUtil.UTF_8) .toUpperCase ()) .append ("\ r \ n"); } returnere responsData; }

Også, hvis den modtagne HttpIndhold er en LastHttpContent, tilføjer vi en farvelbesked og efterfølgende overskrifter, hvis nogen:

StringBuilder prepareLastResponse (HttpRequest anmodning, LastHttpContent trailer) {StringBuilder responseData = ny StringBuilder (); responseData.append ("Farvel! \ r \ n"); hvis (! trailer.trailingHeaders (). er tom ()) {responsData.append ("\ r \ n"); for (CharSequence navn: trailer.trailingHeaders (). navne ()) {for (CharSequence værdi: trailer.trailingHeaders (). getAll (navn)) {responsData.append ("P.S. Trailing Header:"); responsData.append (navn) .append ("=") .append (værdi) .append ("\ r \ n"); }} responseData.append ("\ r \ n"); } returnere responsData; }

3.3. Skrivning af svaret

Nu hvor vores data, der skal sendes, er klar, kan vi skrive svaret til ChannelHandlerContext:

privat ugyldigt writeResponse (ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) {boolean keepAlive = HttpUtil.isKeepAlive (anmodning); FullHttpResponse httpResponse = ny DefaultFullHttpResponse (HTTP_1_1, ((HttpObject) trailer). DekoderResult (). ErSuccess ()? OK: BAD_REQUEST, Unpooled.copiedBuffer (responseData.toString (), CharsetUtil.UTF_8); httpResponse.headers (). sæt (HttpHeaderNames.CONTENT_TYPE, "text / plain; charset = UTF-8"); hvis (keepAlive) {httpResponse.headers (). setInt (HttpHeaderNames.CONTENT_LENGTH, httpResponse.content (). readableBytes ()); httpResponse.headers (). sæt (HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write (httpResponse); hvis (! keepAlive) {ctx.writeAndFlush (Unpooled.EMPTY_BUFFER) .addListener (ChannelFutureListener.CLOSE); }}

I denne metode oprettede vi en FullHttpResponse med HTTP / 1.1-version, tilføjelse af de data, vi havde forberedt tidligere.

Hvis en anmodning skal holdes i live, eller med andre ord, hvis forbindelsen ikke skal lukkes, indstiller vi svarets forbindelse header som holde i live. Ellers lukker vi forbindelsen.

4. Test af serveren

For at teste vores server, lad os sende nogle cURL-kommandoer og se på svarene.

Selvfølgelig, vi er nødt til at starte serveren ved at køre klassen HttpServer før dette.

4.1. FÅ anmodning

Lad os først påkalde serveren og give en cookie med anmodningen:

krølle //127.0.0.1:8080?param1=one

Som svar får vi:

Parameter: PARAM1 = ONE Farvel! 

Vi kan også ramme //127.0.0.1:8080?param1=one fra enhver browser for at se det samme resultat.

4.2. POST-anmodning

Lad os som vores anden test sende en POST med krop prøveindhold:

krølle -d "prøveindhold" -X POST //127.0.0.1:8080

Her er svaret:

PRØVEINDHOLD Farvel!

Denne gang, da vores anmodning indeholdt et organ, serveren sendte den tilbage med store bogstaver.

5. Konklusion

I denne vejledning så vi, hvordan vi implementerede HTTP-protokollen, især en HTTP-server ved hjælp af Netty.

HTTP / 2 i Netty demonstrerer en klientserverimplementering af HTTP / 2-protokollen.

Som altid er kildekoden tilgængelig på GitHub.