Автор оригинала: 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.
Мы добавляем три обработчика в конвейер сервера:
- Netty’s HttpResponseEncoder – для сериализации
- Netty’s HttpRequestDecoder – для десериализации
- Наш собственный 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 .