整理与讲解Netty常用ChannelHandler(2)-https与http2

233 阅读6分钟

引言

Netty自带了许多ChannelHandler来简化开发,当在开发时可以直接使用自带的handler来简化开发,避免重复造轮子,减少程序BUG。

本章用于介绍与https、http2有关的ChannelHandler

HTTPS

SslContext

SslContext是netty自带的用于实现SSL协议的handler

SslContext sslCtx = SslContextBuilder.forServer(
        // 加载证书
        this.getClass().getClassLoader().getResourceAsStream("ssl/server.crt"),
        // 加载私钥
        this.getClass().getClassLoader().getResourceAsStream("ssl/server.key"),
        // 加载密码,没有则为null
        null
).build();
// .....
channel.pipeline().addLast(sslCtx.newHandler(channel.alloc()));

image.png

在使用 HTTPS 时要生成证书

我们可以使用openssl来生成crt证书win

// 先使用rsa算法生成私钥 .key
openssl genrsa -out server.key 1024
// 然后根据私钥生成证书申请文件  .csr
openssl req -new -key server.key -out server.csr
// 根据key与csr生成自签名证书 .crt
openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650

生成私钥:私钥是整个过程的基础,用于加密和解密数据。

生成证书请求(CSR):CSR 包含了你的身份信息和公钥(由私钥派生)。这个文件将用于向证书颁发机构(CA)申请证书。

生成自签名证书:自签名证书使用私钥对 CSR 中的信息进行签名防伪造,生成一个有效的证书。

HTTP2

Http2具有二进制分帧,多路复用,头部压缩、服务器推送等功能,其中服务器必须具有解码二进制分帧头部压缩的功能,并支持多路复用

SSL/TLS + HTTP2 (h2)

HTTP2在默认是要求使用SSL/TLS加密的,所以必须先配置配置SSL。又因为HTTP2在默认是要求使用SSL,所以这种HTTP2实现方式也比较简单

ALPN

应用层协议协商(Application-Layer Protocol Negotiation,简称 ALPN)是由 RFC 7301 定义的一个 TLS 扩展,用于在协商加密连接时识别应用层协议,避免了额外的往返通讯开销。

由于HTTPS与H2都默认使用443端口,且如果客户端并不了解服务器所支持的协议就有可能产生问题。比如,如果服务器只支持 H2,但客户端发送的是 HTTPS 的数据,就会导致通信失败,为了保证双方可以获知应用层将使用的协议,TLS引入了ALPN功能

在使用ALPN时,客户端与服务器可以在SSL/TLS握手阶段协商应用层将使用的协议。

image.png

+------+                                             +------+
|client|                                             |server|
+------+                                             +------+
    |                                                   |
    +--Client Hello:我支持Http1.1与Http2,你选择那个?-->|
    |                                                   |
    |<--Server Hello:http2不错,我选择http2------------- +
    |                                                   |

客户端会在握手时发送一个列表,代表自己支持的协议;服务器会从中选择一个自己想用的协议并返回;此时双方都了解了接下来要使用什么协议通信。

ApplicationProtocolConfig

此类用于为SSL配置一个应用层协议协商器,共有四个需要配置的入参

public ApplicationProtocolConfig(Protocol protocol, SelectorFailureBehavior selectorBehavior,
        SelectedListenerFailureBehavior selectedBehavior, String... supportedProtocols) {
    this(protocol, selectorBehavior, selectedBehavior, toList(supportedProtocols));
}

Protocol

用于选择使用什么来解决协商,目前netty支持NPN,ALPNNPN_AND_ALPN

SelectorFailureBehavior

用于定义失败策略(服务器时),没有找到匹配的协议时触发,目前netty支持FATAL_ALERT(握手失败并警告),NO_ADVERTISE(假装不支持TLS扩展)、CHOOSE_MY_LAST_PROTOCOL(选择列表最后一个协议)

SelectedListenerFailureBehavior

用于定义失败时策略(客户端时),ACCEPT(如果不匹配或不支持TLS扩展,握手继续,并假定接受应用层协议) FATAL_ALERT(如果不匹配或不支持TLS扩展,握手失败,发出警告),CHOOSE_MY_LAST_PROTOCOL(如果不匹配或不支持TLS扩展,握手继续,选择列表最后一个协议)

supportedProtocols

支持那些协议协商,目前netty支持h2 http/1.1 spdy/3.1 spdy/3 spdy/2 spdy/1

ApplicationProtocolNegotiationHandler

ApplicationProtocolNegotiationHandler 是 Netty 中用于处理 TLS 协商结果的一个重要组件。通过这个Handler,我们可以获取到客户端和服务器协商后的应用层协议(如 h2 或 http/1.1),并根据协商结果采取相应的行动。

主要使用方式即为实现configurePipeline方法

// 其中 protocol 为协商结果
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {}

Http2MultiplexHandler

Http2MultiplexHandler是用于实现H2多路复用功能的Handler,使得Netty具有处理H2多路复用的能力,负责将接收到的数据帧分发到相应的子Channel,并将子Channel的响应合并到主Channel,可以同时处理输入与输出

// 常用构造函数
public Http2MultiplexHandler(ChannelHandler inboundStreamHandler) {
    this.inboundStreamHandler = ObjectUtil.checkNotNull(inboundStreamHandler, "inboundStreamHandler"); 
}

image.png

Http2FrameCodec

由于HTTP2具有二进制分帧与头部压缩的特点,netty提供了Http2FrameCodec来实现此支持;而且,此类还负责HTTP2的连接管理,可以同时处理输入与输出

该类需要使用构造器来创建 Http2FrameCodecBuilder.forServer().build()

image.png

明文HTTP2 (h2c)

HTTP2虽然默认要求使用SSL安全连接,但依然允许不使用SSL来运行,这种类型的HTTP2被称为h2c,但这种方式局限性很大,

image.png

主流浏览器并不支持h2c,也就意味着使用h2c就必须自己开发客户端。

与HTTPS、H2都运行在443端口的相同,HTTP与H2C都运行在80端口,所以同样需要一种机制来告知所使用的协议;H2C通过一种称为“协议升级”的方式来实现协商

// req
GET /index HTTP/1.1
Host: xxxxx.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: (SETTINGS payload)
// res
HTTP/1.1 101 Switching Protocols
Connection: Upgrad
Upgrade: h2c
(... HTTP/2 response ...)

根据上面的情况我们可以知道这种方式非常鸡肋,引入“协议升级”机制是为了区分http1.1h2c,但h2c目前只能运行在自己的客户端上,此时使用那种协议是可以人为规定的,如果想在自己的客户端上使用h2c,完全可以直接进行h2c通信,避免“协议升级”浪费带宽。

demo代码

H2

public class H2Initializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        SslContext sslCtx = SslContextBuilder.forServer(
                this.getClass().getClassLoader().getResourceAsStream("ssl/server.crt"),
                this.getClass().getClassLoader().getResourceAsStream("ssl/server.key"),
                null
        ).applicationProtocolConfig(
                new ApplicationProtocolConfig(
                        ApplicationProtocolConfig.Protocol.ALPN,
                        ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
                        ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
                        // 在前面的优先级更高
                        ApplicationProtocolNames.HTTP_2,
                        ApplicationProtocolNames.HTTP_1_1
                )
        ).build();

        ApplicationProtocolNegotiationHandler protocolNegotiationHandler = new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
            @Override
            protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
                switch (protocol) {
                    // 如果是H2
                    case ApplicationProtocolNames.HTTP_2: {
                        ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build());
                        ctx.pipeline().addLast(new Http2MultiplexHandler(new Http2Handler()));
                        break;
                    }
                    // 如果是Http/1.1
                    case ApplicationProtocolNames.HTTP_1_1: {
                        ctx.pipeline().addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(1024 * 1024))
                                .addLast(new HttpHandler());
                    }
                }
            }
        };
        
        // 添加ChandlerHandler
        ch.pipeline()
                .addLast(sslCtx.newHandler(ch.alloc()))
                .addLast(protocolNegotiationHandler);
    }
}
@Slf4j
@ChannelHandler.Sharable
public class Http2Handler extends SimpleChannelInboundHandler<Http2StreamFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame obj) throws Exception {
        log.info("Http2Handler channelRead0...");
        if (obj instanceof Http2HeadersFrame) {
            Http2HeadersFrame headersFrame = (Http2HeadersFrame) obj;
            Http2Headers headers = headersFrame.headers();
            // 如果访问路径为根目录
            log.info("path:{}", headers.path());
            String path = String.valueOf(headers.path());
            if (path.equals("/")) {
                sendIndexHtml(ctx);
            } else if (path.equals("/hello")) {
                sendHelloWorld(ctx);
            }
        }
    }

    private void sendHelloWorld(ChannelHandlerContext ctx) throws IOException {
        byte[] content;
        try (InputStream resource = this.getClass().getClassLoader().getResourceAsStream("hello.html")) {
            assert resource != null;
            content = new byte[resource.available()];
            resource.read(content);
        }
        // 封装http2头帧
        Http2Headers headers = new DefaultHttp2Headers()
                .status(HttpResponseStatus.OK.codeAsText())
                .set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8")
                .set(HttpHeaderNames.CONTENT_LENGTH, "" + content.length)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

        ctx.write(new DefaultHttp2HeadersFrame(headers, false));
        // 封装http2数据帧
        Http2DataFrame dataFrame = new DefaultHttp2DataFrame(ctx.alloc().buffer().writeBytes(content), true);
        ctx.write(dataFrame).addListener(ChannelFutureListener.CLOSE);
    }

    private void sendIndexHtml(ChannelHandlerContext ctx) throws IOException {
        byte[] content;
        try (InputStream resource = this.getClass().getClassLoader().getResourceAsStream("index.html")) {
            assert resource != null;
            content = new byte[resource.available()];
            resource.read(content);
        }
        // 封装http2头帧
        Http2Headers headers = new DefaultHttp2Headers()
                .status(HttpResponseStatus.OK.codeAsText())
                .set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8")
                .set(HttpHeaderNames.CONTENT_LENGTH, "" + content.length)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

        ctx.write(new DefaultHttp2HeadersFrame(headers, false));
        // 封装http2数据帧
        Http2DataFrame dataFrame = new DefaultHttp2DataFrame(ctx.alloc().buffer().writeBytes(content), true);
        ctx.write(dataFrame).addListener(ChannelFutureListener.CLOSE);
    }
}

image.png