基于Netty构建HTTP应用程序

发布时间 2023-08-06 01:06:11作者: 街头卖艺的肖邦

通常HTTP协议通信过程中,客户端和 服务器端的交互过程如下:

  • 客户端(如 Postman工具、浏览器、 Java程序等)向 Server服务端发送 HTTP请求;
  • Server服务端对 HTTP请求进行解析;
  • Server服务端向Client客户端发送 HTTP响应报文;
  • Client客户端解析HTTP响应的应用层协议内容;

Netty中内置的HTTP的解码和编码的处理器

以上交互过程中,Server端将涉及到HTTP请求的解码处理和HTTP响应的编码处理,Netty已经内置了这些解码和编码的处理器,大致如下:

  • HttpRequestDecoder

HTTP请求编码器,这是一个入站处理器,间接的继承了ByteToMessageDecoder,将 ByteBuf缓冲区解码成代表请求的HttpRequest首部实例和HttpContent内容实例;并且,HttpRequestDecoder在解码时会处理好分块(Chunked)类型和固定长度(Content-Length)类型的 HTTP请求报文;

  • HttpResponseEncoder

HTTP响应编码器,它把代表响应的HttpResonse首部实例和HttpContent内容实例编码成ByteBuf字节流,它是一个出站处理器;

  • HttpServerCodec

HTTP的编解码器,它是HttpRequestDecoder解码器和HttpResponseEncoder编码器的结合体,换句话说,HttpServerCodec可以替代HttpRequestDecoder和HttpResponseEncoder;

  • HttpObjectAggregator

由于HTTP的请求和响应可能由许多部分组成,因此开发人员需要聚合它们以形成完整的消息,为了消除这项繁琐的任务,Netty提供了一个聚合器,它可以将多个消息部分合并为FullHttpRequest或者FullHttpResponse消息,通过这样的方式,获取完整的消息内容;

  • QueryStringDecoder

把HTTP的请求URI分割成Path路径和Key-Value参数键值对,不继承任何InboundHandler和OutboundHandler的基类,对于同一次请求,在自定义的Handler只有使用一次有效;

 

基于Netty的HTTP请求的处理流程

基于Netty的 HTTP请求的处理流程,大致如下:

  • 二进制的 HTTP数据包从 Channel通道入站后,首先进入 Pipeline流水线的是ByteBuf字节流;
  • HttpRequestDecoder首先将ByteBuf缓冲区中的请求行(RequestLine)和请求头Header解析成 HttpRequest首部对象,传入到 HttpObjectAggregator;然后再将 HTTP数据包的请求体Body解析出HttpContent对象(可能是多个),传入到 HttpObjectAggregator聚合器;解码完成之后,如果没有更多的请求体内容, HttpRequestDecoder会传递一个LastHttpContent结束实例到聚合器 HttpObjectAggregator,表示 HTTP请求数据已经解析完成;
  • 当 HttpObjectAggregator发现有入站包为LastHttpContent实例入站时,代表 HTTP请求数据协议解析完成,此时,会将所收到的全部 HttpObject实例,封装一个FullHttpRequest整体请求实例,发送给下一站,这里的下一站基本上为业务处理器;

 

Netty中HTTP请求和响应的组成部分

一个HTTP 请求/响应可能由多个数据部分组成,并且它总是以一个LastHttpContent部分作为结束;FullHttpRequest和FullHttpResponse消息是特殊的子类型,分别代表了完整的请求和响应;所有类型的HTTP消息都实现了HttpObject接口;

Netty中HTTP请求组成部分

Netty内置的与HTTP请求报文相对应的,大致如下几个:

  • FullHttpRequest

包含整个 HTTP请求的信息,包含对 HttpRequest首部和HttpContent请求体的组合;

  • HttpRequest

请求首部,主要包含对 HTT请求行(Request Line)和请求头Header的组合;

  • HttpContent

HTT请求体Body进行封装,本质上就是一个ByteBuf缓冲区实例;如果ByteBuf的长度是固定的,请求的Body过大,可能包含多个HttpContent,解码的时候,最后一个解码返回对象为LastHttpContent(空的HttpContent),表示对 Body的解码已经结束;

  • HttpMethod

主要是对 HTTP请求方法 Method的封装;

  • HttpVersion

对HTTP版本Version的封装,该类定义了HTTP/1.0和HTTP/1.1两个协议版本;

  • HttpHeaders

包含对 HTTP报文请求头 Header的封装及相关操作;

 

HTTP报文各部分所对应的Netty类,如下

Netty的HttpRequest首部类中有一个 String类型的uri成员变量,主要是对请求URI的封装,该成员包含了HTTP请求的Path路径和跟随在其后的请求参数;

 

Netty中HTTP响应组成部分

FullHttpResponse与FullHttpRequest不同的是,FullHttpResponse中第一个部分为HttpResponse;

 

基于Netty的HTTP响应编码的流程

Netty的HTTP响应的处理流程,只需在流水线装配HttpResponseEncoder编码器即可,该编码器是一个出站处理器,有以下特点:

  • 该编码器输入的是FullHttpResponse响应实例,输出是ByteBuf字节缓冲器,后面的处理器会将该ByteBuf数据写入Channel通道,最终会被发送到HTTP客户端;
  • 该编码器按照HTTP协议对入站FullHttpResponse实例的响应行、响应头、 响应体进行序列化,通过Header响应头去判断是否含有Content-Length头或者Trunked头,然后将响应体Body按照相应的长度规则,对内容进行序列化;

大致的Pipeline的装配代码如下:

ChannelPipeline pipeline = ch.pipeline();
// HTTP请求解码器
pipeline.addLast(new HttpRequestDecoder());
// HTTP请求聚合器
pipeline.addLast(new HttpObjectAggregator(65535));
// HTTP响应编码器
pipeline.addLast(new HttpResponseEncoder());
// 自定义的Handler
pipeline.addLast(new HttpEchoHandler());

 

可以用HttpServerCodec替代HttpRequestDecoder和HttpResponseEncoder,如下:

ChannelPipeline pipeline = ch.pipeline();
// HTTP请求解码器和响应编码器
pipeline.addLast(new HttpServerCodec());
// HTTP请求聚合器
pipeline.addLast(new HttpObjectAggregator(65535));
// 自定义的Handler
pipeline.addLast(new HttpEchoHandler());

 

HTTP压缩

Netty为压缩和解压缩提供了ChannelHandler实现,它们同时支持gzip和deflate编码;

注:当使用HTTP时,开启压缩功能以尽可能多地减小传输数据的大小,但压缩会带来一些CPU 时钟周期上的开销;

 

大致的Pipeline的装配代码如下:

客户端装配方式

ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
// 处理来自服务器的压缩内容
pipeline.addLast(new HttpContentDecompressor());

 

服务端装配方式

ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
// 添加HttpContentCompressor来压缩数据
pipeline.addLast(new HttpContentCompressor());

 

使用HTTPS协议安全协议通信实战

启用HTTPS只需要将SslHandler添加到ChannelPipeline的ChannelHandler组合中;

流水线装配方式如下:

public class HttpCodecInitializer extends ChannelInitializer<Channel> {
    private final SSLContext sslContext;
    private final boolean isClient;

    public HttpCodecInitializer(SSLContext sslContext, boolean isClient) {
        this.sslContext = sslContext;
        this.isClient = isClient;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //通过上下文实例,创建服务端的 SSL 引擎
        SSLEngine sslEngine = sslContext.createSSLEngine();
        //在握手的时候,使用服务端模式
        sslEngine.setUseClientMode(false);
        //单向认证:在服务端设置不需要验证对端身份,无需客户端证实自己的身份
        sslEngine.setNeedClientAuth(false);
        //创建SslHandler处理器,并加入到流水线
        pipeline.addLast(new SslHandler(sslEngine));

        if (isClient) {
            pipeline.addLast(new HttpClientCodec());
        } else {
            pipeline.addLast(new HttpServerCodec());
        }

        pipeline.addLast(new HttpContentCompressor());
        // HTTP请求聚合器
        pipeline.addLast(new HttpObjectAggregator(65535));

        // 自定义的Handler
        pipeline.addLast(new HttpEchoHandler());
    }
}