如何用Netty构建一个HTTP服务器?

原文参见:如何用Netty构建一个HTTP服务器?

1.概述

本文介绍如何用Netty构建一个简单的HTTP服务器,体验一下如何通过Netty进行Java网络应用开发。

2.服务器引导

真正开始之前,请先了解一下Netty基本概念,诸如channel、handler、encoder和decoder等(译注:不在本文介绍)。

最先介绍的是服务器引导(Server Bootstraping)部分。这里的服务器另一篇文章中的Simple Protocol Server类似:

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)。

代码为服务器的ChannelPipeline添加三个Handler:

  1. Netty的HttpResponseEncoder –用于序列化

  2. Netty的HttpRequestDecoder –用于反序列化

  3. 需自己实现的CustomHttpServerHandler –用于定义服务器的行为

最后一个Handler是实现HTTP服务的重点,来看一下它的实现。

3. CustomHttpServerHandler

我们这是实现的自定义Handler会用于处理入站数据并发送响应。分析、了解一下其工作原理。

3.1. Handler的结构

CustomHttpServerHandler扩展了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();
    }
}

正如方法名称所示,处理完pipeline中的最后一条消息之后,channelReadComplete冲刷(flush)handlerContext,以便可处理下一条传入的消息。方法exceptionCaught用于可能发生的异常处理。

目前为止,所看到的还仅仅只是通用部分的代码。

下来进入最有意思的部分,即channelRead0的实现。

3.2. 读取channel的数据

样例代码很简单,服务器将请求正文和查询参数(如果有的话)转换为大写。在此提醒一下样例代码从请求数据产生响应消息的逻辑:样例代码的大写转化逻辑很简单,仅用于演示目的,用于了解如何使用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);
    }
}

可以看到,从channel收到HttpRequest后,代码首先检查请求是否期望得到一个“100 Continue”,如果是这种情况,代码立即回写一个空响应,状态为CONTINUE:

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

此后,handler初始化用于响应的字符串,将请求的查询参数添加到该字符串并原样发回。

接下来,定义方法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,则添加"goodbye"消息和trailing headers(如果有的话):

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

此方法中创建了HTTP/1.1版本的FullHttpResponse,添加了上面准备好的响应数据。

如请求需保持活动状态,即保持连接不关闭,需将响应的header设置为keep-alive。不然的话连接将关闭。

4.测试服务器

为了测试我们实现的服务器,我们发送一些cURL命令来查看响应。

在此之前,需要先运行HttpServer类来启动服务器。

4.1.GET请求

发起一个服务器GET请求,此请求提供一个参数:

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请求

第二个测试发送正文(body)内容为“sample content”的POST 请求:

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

 

响应为:

SAMPLE CONTENT Good Bye!

 

这次发送的请求包含了请求体(body),故服务器以大写形式将其内容返回。

5.最后

本文介绍了如何实现HTTP协议,即通过Netty来实现一个HTTP服务器。

Netty中的HTTP/2演示了HTTP/2协议的客户端-服务器实现。

与以往一样,源代码可以从GitHub上获得

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值