HTTP сървър с Netty

1. Общ преглед

В този урок ще приложим прост сървър с главни букви през HTTP с Netty , асинхронна рамка, която ни дава гъвкавостта да разработваме мрежови приложения в Java.

2. Обучение на сървъра

Преди да започнем, трябва да сме наясно с основните концепции на Netty, като канал, манипулатор, енкодер и декодер.

Тук ще преминем направо към зареждане на сървъра, което е предимно същото като обикновения сървър за протоколи:

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

И така, тук само childHandler се различава според протокола, който искаме да приложим , който е HTTP за нас.

Добавяме три манипулатора към конвейера на сървъра:

  1. Netty 's HttpResponseEncoder - за сериализация
  2. Netty 's HttpRequestDecoder - за десериализация
  3. Нашият собствен CustomHttpServerHandler - за дефиниране на поведението на нашия сървър

Нека разгледаме последния манипулатор в детайли по-нататък.

3. CustomHttpServerHandler

Работата на нашия персонализиран манипулатор е да обработва входящи данни и да изпраща отговор.

Нека го разделим, за да разберем как работи.

3.1. Структура на манипулатора

CustomHttpServerHandler разширява абстрактния SimpleChannelInboundHandler на Netty и прилага своите методи на жизнения цикъл:

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

Както подсказва името на метода, channelReadComplete изтрива контекста на манипулатора, след като последното съобщение в канала е консумирано, така че да е достъпно за следващото входящо съобщение. Методът izuzetCaught е за обработка на изключения, ако има такива.

Досега всичко, което видяхме, е кодът на шаблона.

Сега нека да продължим с интересните неща, изпълнението на channelRead0 .

3.2. Четене на канала

Нашият случай на употреба е прост, сървърът просто ще преобразува тялото на заявката и параметрите на заявката, ако има такива, в главни букви. Тук е дума на внимание при отразяване на данните за заявките в отговора - ние правим това само за демонстрационни цели, за да разберем как можем да използваме Netty за внедряване на HTTP сървър.

Тук ще консумираме съобщението или заявката и ще настроим отговора му, както е препоръчано от протокола (имайте предвид, че RequestUtils е нещо, което ще напишем само за момент):

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

Както виждаме, когато нашият канал получи HttpRequest , той първо проверява дали заявката очаква статус 100 Continue. В този случай незабавно пишем обратно с празен отговор със статус ПРОДЪЛЖАВА :

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

След това манипулаторът инициализира низ, който да бъде изпратен като отговор, и добавя параметрите на заявката на заявката към него, за да бъде изпратен обратно както е.

Нека сега дефинираме метода formatParams и го поставим в помощен клас RequestUtils, за да направим това:

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

На следващо място, при получаване на HttpContent , ние приемаме тялото на поискване и да го превърнат в главни букви :

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

Също така, ако полученият HttpContent е LastHttpContent , ние добавяме съобщение за сбогом и последващи заглавки, ако има такива:

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. Писане на отговора

Сега, когато данните ни за изпращане са готови, можем да напишем отговора на 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); } }

В този метод създадохме FullHttpResponse с HTTP / 1.1 версия, добавяйки данните, които сме подготвили по-рано.

Ако дадена заявка трябва да се поддържа жива, или с други думи, ако връзката не трябва да бъде затворена, ние задаваме заглавката на връзката на отговора като keep-alive . В противен случай затваряме връзката.

4. Тестване на сървъра

За да тестваме нашия сървър, нека изпратим някои команди cURL и да разгледаме отговорите.

Разбира се, трябва да стартираме сървъра, като стартираме класа HttpServer преди това .

4.1. ВЗЕМЕТЕ заявка

Нека първо извикаме сървъра, като предоставим бисквитка с искането:

curl //127.0.0.1:8080?param1=one

Като отговор получаваме:

Parameter: PARAM1 = ONE Good Bye! 

Също така можем да натиснем //127.0.0.1:8080?param1=one от всеки браузър, за да видим същия резултат.

4.2. POST заявка

Като нашия втори тест, нека изпратим POST със съдържание на телесна проба :

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

Ето отговора:

SAMPLE CONTENT Good Bye!

Този път, тъй като нашата заявка съдържаше тяло, сървърът го изпрати обратно с главни букви .

5. Заключение

В този урок видяхме как да внедрим HTTP протокола, по-специално HTTP сървър, използващ Netty.

HTTP / 2 в Netty демонстрира реализация на клиент-сървър на протокола HTTP / 2.

Както винаги, изходният код е достъпен в GitHub.