Рубрики
Без рубрики

HTTP-сервер с Netty

Изучите, как реализовать протокол HTTP, в частности HTTP-сервер, использующий Netty.

Автор оригинала: Sampada Wagde.

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

Итак, здесь только дочерний обработчик отличается в соответствии с протоколом , который мы хотим реализовать , который для нас является HTTP.

Мы добавляем три обработчика в конвейер сервера:

  1. Netty’s HttpResponseEncoder – для сериализации
  2. Netty’s HttpRequestDecoder – для десериализации
  3. Наш собственный CustomHttpServerHandler – для определения поведения нашего сервера

Давайте подробнее рассмотрим последний обработчик.

3. Пользовательский обработчик Http – Сервера

Работа нашего пользовательского обработчика заключается в обработке входящих данных и отправке ответа.

Давайте разберем его, чтобы понять, как он работает.

3.1. Структура обработчика

Пользовательский обработчик Http-сервера расширяет абстракцию Netty SimpleChannelInboundHandler и реализует его методы жизненного цикла:

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 сбрасывает контекст обработчика после использования последнего сообщения в канале, чтобы он был доступен для следующего входящего сообщения. Метод exception Catched предназначен для обработки исключений, если таковые имеются.

До сих пор все, что мы видели, – это шаблонный код.

Теперь давайте перейдем к интересному, реализации 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);
    }
}

Как мы видим, когда наш канал получает Http-запрос , он сначала проверяет, ожидает ли запрос 100 Продолжения статуса. В этом случае мы немедленно пишем ответ с пустым ответом со статусом ПРОДОЛЖИТЬ :

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

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

Теперь давайте определим метод format Params и поместим его в 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;
}

Затем, получив Http-контент , мы берем тело запроса и преобразуем его в верхний регистр :

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

Кроме того, если полученный Http-контент является 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, добавив данные, которые мы подготовили ранее.

Если запрос должен быть сохранен, или, другими словами, если соединение не должно быть закрыто, мы устанавливаем заголовок ответа connection как keep-alive . В противном случае мы закрываем соединение.

4. Тестирование сервера

Чтобы протестировать наш сервер, давайте отправим несколько команд cURL и посмотрим на ответы.

Конечно, нам нужно запустить сервер, запустив класс HTTPServer до этого .

4.1. ПОЛУЧИТЬ запрос

Давайте сначала вызовем сервер, предоставив файл cookie с запросом:

curl http://127.0.0.1:8080?param1=one

В качестве ответа мы получаем:

Parameter: PARAM1 = ONE

Good Bye!

Мы также можем ударить http://127.0.0.1:8080?param1=one из любого браузера, чтобы увидеть тот же результат.

4.2. Запрос на публикацию

В качестве нашего второго теста давайте отправим СООБЩЕНИЕ с телом образцом содержимого :

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

Вот ответ:

SAMPLE CONTENT
Good Bye!

На этот раз, поскольку наш запрос содержал тело, сервер отправил его обратно в верхнем регистре .

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

В этом уроке мы рассмотрели, как реализовать протокол HTTP, в частности HTTP-сервер с использованием Netty.

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

Как всегда, исходный код доступен на GitHub .