netty系列之:手持framecodec神器,建立多路複用http2客戶端

flydean發表於2021-12-09

簡介

在之前的文章中,我們實現了支援http2的netty伺服器,並且使用支援http2的瀏覽器成功的進行訪問。雖然瀏覽器非常通用,但是有時候我們也需要使用特定的netty客戶端去和伺服器進行通訊。

今天我們來探討一下netty客戶端對http2的支援。

配置SslContext

雖然http2並不強制要求支援TLS,但是現代瀏覽器都是需要在TLS的環境中開啟http2,所以對於客戶端來說,同樣需要配置好支援http2的SslContext。客戶端和伺服器端配置SslContext的內容沒有太大的區別,唯一的區別就是需要呼叫SslContextBuilder.forClient()而不是forServer()方法來獲取SslContextBuilder,建立SslContext的程式碼如下:

SslProvider provider =
                    SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK;
            sslCtx = SslContextBuilder.forClient()
                  .sslProvider(provider)
                  .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
                  // 因為我們的證書是自生成的,所以需要信任放行
                  .trustManager(InsecureTrustManagerFactory.INSTANCE)
                  .applicationProtocolConfig(new ApplicationProtocolConfig(
                          Protocol.ALPN,
                          SelectorFailureBehavior.NO_ADVERTISE,
                          SelectedListenerFailureBehavior.ACCEPT,
                          ApplicationProtocolNames.HTTP_2,
                          ApplicationProtocolNames.HTTP_1_1))
                  .build();

如果使用SSL,那麼ssl handler必須是pipline中的第一個handler,所以將SslContext加入到pipline中的程式碼如下:

ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc()));

客戶端的handler

使用Http2FrameCodec

netty的channel預設只能接收ByteBuf訊息,對於http2來說,底層傳輸的是一個個的frame,直接操作底層的frame對於普通程式設計師來說並不是特別友好,所以netty提供了一個Http2FrameCodec來對底層的http2 frame進行封裝成Http2Frame物件,方便程式的處理。

在伺服器端我們使用Http2FrameCodecBuilder.forServer()來建立Http2FrameCodec,在客戶端我們使用Http2FrameCodecBuilder.forClient()來建立Http2FrameCodec:

Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient()
            .initialSettings(Http2Settings.defaultSettings())
            .build();

然後將其加入到pipline中即可使用:

        ch.pipeline().addLast(http2FrameCodec);

Http2MultiplexHandler和Http2MultiplexCodec

我們知道對於http2來說一個TCP連線中可以建立多個stream,每個stream又是由多個frame來組成的。考慮到多路複用的情況,netty可以為每一個stream建立一個單獨的channel,對於新建立的每個channel來說,都可以使用netty的ChannelInboundHandler來對channel的訊息進行處理,從而提升netty處理http2的效率。

而這個對stream建立新channel的支援,在netty中有兩個專門的類,他們是Http2MultiplexHandler和Http2MultiplexCodec。

他們的功能是一樣的,Http2MultiplexHandler繼承自Http2ChannelDuplexHandler,它必須和 Http2FrameCodec一起使用。而Http2MultiplexCodec本身就是繼承自Http2FrameCodec,已經結合了Http2FrameCodec的功能。

public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler

@Deprecated
public class Http2MultiplexCodec extends Http2FrameCodec 

但是通過檢查原始碼,我們發現Http2MultiplexCodec是不推薦使用的API,所以這裡我們主要介紹Http2MultiplexHandler。

對於Http2MultiplexHandler來說,每次新建立一個stream,都會建立一個新的對應的channel,應用程式使用這個新建立的channel來傳送和接收Http2StreamFrame。

新建立的子channel會被註冊到netty的EventLoop中,所以對於一個有效的子channel來說,並不是立刻就會被匹配到HTTP/2 stream上去,而是當第一個Http2HeadersFrame成功被髮送或者接收之後,才會觸發Event事件,進而進行繫結操作。

因為是子channel,所以對於connection level的事件,比如Http2SettingsFrame 和 Http2GoAwayFrame會首先被父channel進行處理,然後再廣播到子channel中進行處理。

同時,雖然Http2GoAwayFrame 和 Http2ResetFrame表示遠端節點已經不再接收新的frame了,但是因為channel本身還可能有queue的訊息,所以需要等待Channel.read()為空之後,才會進行關閉操作。

另外對於子channel來說,因為不能知道connection-level流控制window,所以如果有溢位的訊息會被快取在父channel的buff中。

有了Http2MultiplexHandler,將其加入client的pipline就可以讓客戶端支援多路的channel了:

ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() {
            @Override
            protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
                // 處理inbound streams
                log.info("Http2MultiplexHandler接收到訊息: {}",msg);
            }
        }))

使用子channel傳送訊息

從上面的介紹我們知道,一旦使用了Http2MultiplexHandler,那麼具體的訊息處理就是在子channel中了。那麼怎麼才能從父channel中獲取子channel,然後使用子channel來傳送資訊呢?

netty提供Http2StreamChannelBootstrap類,它提供了open方法,來建立子channel:

        final Http2StreamChannel streamChannel;
        try {
            if (ctx.handler() instanceof Http2MultiplexCodec) {
                streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream();
            } else {
                streamChannel = ((Http2MultiplexHandler) ctx.handler()).newOutboundStream();
            }

我們要做的就是呼叫這個方法,來建立子channel:

final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow();

然後將自定義的,專門處理Http2StreamFrame的Http2ClientStreamFrameHandler,新增到子channel的pipline中即可:

final Http2ClientStreamFrameHandler streamFrameResponseHandler =
                    new Http2ClientStreamFrameHandler();
streamChannel.pipeline().addLast(streamFrameResponseHandler);

準備完畢,構建http2訊息,使用streamChannel進行傳送:

// 傳送HTTP2 get請求
            final DefaultHttp2Headers headers = new DefaultHttp2Headers();
            headers.method("GET");
            headers.path(PATH);
            headers.scheme(SSL? "https" : "http");
            Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, true);
            streamChannel.writeAndFlush(headersFrame);

總結

以上就是使用netty的framecode構建http2的客戶端和伺服器端進行通訊的基本操作了。

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/32-netty-http2client-framecodec/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章