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.
Мы добавляем три обработчика в конвейер сервера:
HttpResponseEncoder
Нетти — для сериализацииHttpRequestDecoder
Нетти — для десериализации- Наш собственный
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
сбрасывает контекст обработчика после использования последнего сообщения в канале, чтобы он был доступен для следующего входящего сообщения. Метод exceptionCaught
предназначен для обработки исключений, если таковые имеются.
До сих пор все, что мы видели, это шаблонный код.
Теперь давайте приступим к интересным вещам, реализации 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 . В этом случае мы немедленно пишем пустой ответ со статусом 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<String, List<String>> params = queryStringDecoder.parameters();
if (!params.isEmpty()) {
for (Entry<String, List<String>> p : params.entrySet()) {
String key = p.getKey();
List<String> 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. ПОЛУЧИТЬ запрос
Давайте сначала вызовем сервер, предоставив 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. Почтовый запрос
В качестве нашего второго теста давайте отправим POST с содержимым образца
тела :
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 .