Давным-давно в одной далёкой-далёкой... проекте, понадобилось мне сделать обработку http-запросов на Netty. К сожалению, стандартных удобных механизмов для маппинга http-запросов в Netty не нашлось (да и этот фреймвёрк совсем не для того), поэтому, было решено реализовать собственный механизм.


Если читатель начал беспокоиться о судьбе проекта, то не стоит, с ним всё хорошо, т.к. в дальнейшем было решено переписать веб-сервис на фреймвёрке более заточенном под RESTful сервисы, без использования собственных велосипедов. Но наработки остались, и они могут быть кому-нибудь полезными, поэтому хотелось бы ими поделиться.

Netty — это фреймвёрк, позволяющий разрабатывать высокопроизводительные сетевые приложения. Подробнее о нём можно прочитать на сайте проекта.
Для создания сокет-серверов Netty предоставляет весьма удобный функционал, но для создание REST-серверов данный функционал, на мой взгляд, является не очень удобным.


Обработка запросов с использованием стандартного механизма Netty


Для обработки запросов в Netty необходимо отнаследоваться от класса ChannelInboundHandlerAdapter и переопределить метод channelRead.


public class HttpMappingHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    }
}

Для получения необходимой при обработке http-запросов информации объект msg можно привести к HttpRequest.


HttpRequest request = (HttpRequest) msg;

После этого можно получить какую-либо информацию из этого запроса. Например, URL-адрес запроса.


String uri = request.uri();

Тип запроса.


HttpMethod httpMethod = request.method();

И контент.


ByteBuf byteBuf = ((HttpContent) request).content();

Контентом может быть, например, json, переданный в теле POST-запроса. ByteBuf — это класс из библиотеки Netty, поэтому json-парсеры вряд ли смогут с ним работать, но его очень просто можно привести к строке.


String content = byteBuf.toString(StandardCharsets.UTF_8);

Вот, в общем-то, и всё. Используя указанные выше методы, можно обрабатывать http-запросы. Правда, обрабатывать всё придётся в одном месте, а именно в методе channelRead. Даже если разнести логику обработки запросов по разным методам и классам, всё равно придётся сопоставлять URL с этими методами где-то в одном месте.


Маппинг запросов


Что ж, как видим, вполне можно реализовать маппинг http-запросов, используя стандартный функционал Netty. Правда, будет это не очень удобно. Хотелось бы как-то полностью разнести обработку http-запросов по разным методам (например, как это сделано в Spring). С помощью рефлексии была предпринята попытка реализовать подобный подход. Получилась из этого библиотека num. С её исходным кодом можно ознакомиться по ссылке.
Для использования маппинга запросов с помощью библиотеки num достаточно отнаследоваться от класса AbstractHttpMappingHandler, после чего в этом классе можно будет создавать методы-обработчики запросов. Главное требование к данным методам — это чтобы они возвращали FullHttpResponse или его наследников. Показать, по какому http-запросу будет вызываться данный метод, можно с помощью аннотаций:


  • @Get
  • @Post
  • @Put
  • @Delete

Имя аннотации показывает то, какой тип запроса будет вызван. Поддерживается четыре типа запросов: GET, POST, PUT и DELETE. В качестве параметра value в аннотации необходимо указать URL-адрес, при обращении на который будет вызываться нужный метод.


Пример того, как будет выглядеть обработчик GET-запроса, который возвращает строку Hello, world!.


public class HelloHttpHandler extends AbstractHttpMappingHandler {

      @Get("/test/get")
      public DefaultFullHttpResponse test() {
          return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer("Hello, world!", StandardCharsets.UTF_8));
      }
}

Параметры запросов


Передача параметров из запроса в метод-обработчик осуществляется также с помощью аннотаций. Для этого можно воспользоваться одной из следующих аннотаций:


  • @PathParam
  • @QueryParam
  • @RequestBody

@PathParam


Для передачи path-параметров используется аннотация @PathParam. При её использовании в качестве параметра value аннотации необходимо указать название параметра. Кроме того, название параметра необходимо указать и в URL запроса.


Пример того, как будет выглядеть обработчик GET-запроса, в который передаётся path-параметр id и который возвращает этот параметр.


public class HelloHttpHandler extends AbstractHttpMappingHandler {

      @Get("/test/get/{id}")
      public DefaultFullHttpResponse test(@PathParam(value = "id") int id) {
          return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer(id, StandardCharsets.UTF_8));
      }
}

@QueryParam


Для передачи query-параметров используется аннотация @QueryParam. При её использовании в качестве параметра value аннотации необходимо указать название параметра. Обязательностью параметра можно управлять с помощью параметра аннотации required.


Пример того, как будет выглядеть обработчик GET-запроса, в который передаётся query-параметр message и который возвращает этот параметр.


public class HelloHttpHandler extends AbstractHttpMappingHandler {

      @Get("/test/get")
      public DefaultFullHttpResponse test(@QueryParam(value = "message") String message) {
          return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer(message, StandardCharsets.UTF_8));
      }
}

@RequestBody


Для передачи тела POST-запросов используется аннотация @RequestBody. Поэтому и использовать её разрешается только в POST-запросах. Предполагается, что в качестве тела запроса будут передаваться данные в формате json. Поэтому для использования @RequestBody необходимо в конструктор класса-обработчика передать реализацию интерфейса JsonParser, которая будет заниматься парсингом данных из тела запроса. Также в библиотеке уже имеется реализация по умолчанию JsonParserDefault. В качестве парсера данная реализация использует jackson.


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


public class HelloHttpHandler extends AbstractHttpMappingHandler {

      @Post("/test/post")
      public DefaultFullHttpResponse test(@RequestBody Message message) {
          return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                Unpooled.copiedBuffer("{id: '" + message.getId() +"', msg: '" + message.getMessage() + "'}",
                        StandardCharsets.UTF_8));
      }
}

Класс Message выглядит следующим образом.


public class Message {

    private int id;
    private String message;

    public Message() {
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Обработка ошибок


Если при обработке запросов возникнет какой-либо Exception и он не будет перехвачен в коде методов-обработчиков, то вернётся ответ с 500-ым кодом. Для того чтобы написать какую-то свою логику по обработке ошибок, достаточно переопределить в классе-обработчике метод exceptionCaught.


public class HelloHttpHandler extends AbstractHttpMappingHandler {

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR));
    }
}

Заключение


Вот, в общем-то, и всё. Надеюсь, это было интересно и будет кому-нибудь полезным.




Код примера http-сервера на Netty с использованием библиотеки num доступен по ссылке.

Комментарии (7)


  1. tuxi
    12.01.2019 22:13

    Реализация обработки разных методов хттп запросов практически в любом сервлет-контейнере представлена. Это упростило бы разработку. У меня вопрос по делу: Вы с вебсокетами на netty пробовали играться? Насколько стабильно все работает?


    1. 1_van
      12.01.2019 22:37

      Пробовал. Проблем не возникало, вроде бы всё стабильно, а ещё весьма удобно.:)


      1. tuxi
        12.01.2019 22:51

        Что удобно — это понятно :) Мне интересно, насколько устойчиво держится канал со страницей, например в гугл хроме, которую оставили открытой и активной, но нет никакой пользовательской активности. Таких кейсов не было?


        1. APXEOLOG
          12.01.2019 23:59

          У меня как раз такой кейс. По моему опыту (google chrome <-> netty websocket server):


          • Если действие происходит локально у меня на машине (дев сервер), то никаких проблем нет, сессия замечательно держится хоть сутками без активности
          • Если сервер развернут на Амазоне (EBS), то действительно соединение без активности рвется довольно быстро. Но я подозреваю что это проблемы в настройке EBS'кого nginx'a, однако времени проверить этого у меня пока не было


          1. tuxi
            13.01.2019 00:04

            Вот такая же вялотекущая фихня. Не амазон, но тоже nginx. Пока не критично, но начинает, как говорится, задевать самолюбие.


        1. 1_van
          13.01.2019 00:01

          Не-а. Были только сервер, написанный на Netty и клиенты на Netty. Там всё было вообще отлично.:)


        1. BugM
          13.01.2019 00:20

          Хорошо все держится, если keep alive слать регулярно. Без keep alive все плохо.