Servidor HTTP con Netty

1. Información general

En este tutorial, vamos a implementar un servidor simple en mayúsculas sobre HTTP con Netty , un marco asincrónico que nos da la flexibilidad para desarrollar aplicaciones de red en Java.

2. Bootstrapping del servidor

Antes de comenzar, debemos conocer los conceptos básicos de Netty, como canal, controlador, codificador y decodificador.

Aquí saltaremos directamente a arrancar el servidor, que es casi lo mismo que un servidor de protocolo simple:

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

Entonces, aquí solo el childHandler difiere según el protocolo que queremos implementar , que es HTTP para nosotros.

Agregamos tres controladores a la canalización del servidor:

  1. HttpResponseEncoder de Netty - para serialización
  2. HttpRequestDecoder de Netty - para deserialización
  3. Nuestro propio CustomHttpServerHandler - para definir el comportamiento de nuestro servidor

Veamos el último controlador en detalle a continuación.

3. CustomHttpServerHandler

El trabajo de nuestro controlador personalizado es procesar los datos entrantes y enviar una respuesta.

Analicémoslo para entender su funcionamiento.

3.1. Estructura del controlador

CustomHttpServerHandler extiende el SimpleChannelInboundHandler abstracto de Netty e implementa sus métodos de ciclo de vida:

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

Como sugiere el nombre del método, channelReadComplete vacía el contexto del controlador después de que se haya consumido el último mensaje en el canal para que esté disponible para el siguiente mensaje entrante. El método exceptionCaught es para manejar excepciones, si las hay.

Hasta ahora, todo lo que hemos visto es el código estándar.

Ahora sigamos con lo interesante, la implementación de channelRead0 .

3.2. Leyendo el canal

Nuestro caso de uso es simple, el servidor simplemente transformará el cuerpo de la solicitud y los parámetros de la consulta, si los hay, a mayúsculas. Una advertencia aquí sobre cómo reflejar los datos de la solicitud en la respuesta: lo hacemos solo con fines de demostración, para comprender cómo podemos usar Netty para implementar un servidor HTTP.

Aquí, consumiremos el mensaje o la solicitud y configuraremos su respuesta según lo recomendado por el protocolo (tenga en cuenta que RequestUtils es algo que escribiremos en un momento):

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

Como podemos ver, cuando nuestro canal recibe una HttpRequest , primero verifica si la solicitud espera un estado 100 Continuar. En ese caso, escribimos inmediatamente con una respuesta vacía con un estado de CONTINUAR :

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

Después de eso, el controlador inicializa una cadena que se enviará como respuesta y agrega los parámetros de consulta de la solicitud para que se devuelva tal cual.

Definamos ahora el método formatParams y colóquelo en una clase auxiliar RequestUtils para hacer eso:

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

A continuación, al recibir un HttpContent , tomamos el cuerpo de la solicitud y lo convertimos a mayúsculas :

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

Además, si el HttpContent recibido es un LastHttpContent , agregamos un mensaje de despedida y encabezados finales, si los hay:

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. Escribir la respuesta

Ahora que nuestros datos para ser enviados están listos, podemos escribir la respuesta al 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); } }

En este método, creamos una FullHttpResponse con la versión HTTP / 1.1, agregando los datos que habíamos preparado anteriormente.

Si una solicitud debe mantenerse viva, o en otras palabras, si la conexión no debe cerrarse, configuramos el encabezado de conexión de la respuesta como keep-alive . De lo contrario, cerramos la conexión.

4. Prueba del servidor

Para probar nuestro servidor, enviemos algunos comandos cURL y miremos las respuestas.

Por supuesto, necesitamos iniciar el servidor ejecutando la clase HttpServer antes de esto .

4.1. OBTENER Solicitud

Primero invoquemos el servidor, proporcionando una cookie con la solicitud:

curl //127.0.0.1:8080?param1=one

Como respuesta, obtenemos:

Parameter: PARAM1 = ONE Good Bye! 

También podemos presionar //127.0.0.1:8080?param1=one desde cualquier navegador para ver el mismo resultado.

4.2. Solicitud POST

Como segunda prueba, enviemos un POST con contenido de muestra del cuerpo :

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

Esta es la respuesta:

SAMPLE CONTENT Good Bye!

Esta vez, dado que nuestra solicitud contenía un cuerpo, el servidor lo devolvió en mayúsculas .

5. Conclusión

En este tutorial, vimos cómo implementar el protocolo HTTP, particularmente un servidor HTTP que usa Netty.

HTTP / 2 en Netty demuestra una implementación cliente-servidor del protocolo HTTP / 2.

Como siempre, el código fuente está disponible en GitHub.