netty系列之:搭建客戶端使用http1.1的方式連線http2伺服器

flydean發表於2021-11-03

簡介

對於http2協議來說,它的底層跟http1.1是完全不同的,但是為了相容http1.1協議,http2提供了一個從http1.1升級到http2的方式,這個方式叫做cleartext upgrade,也可以簡稱為h2c。

在netty中,http2的資料對應的是各種http2Frame物件,而http1的資料對應的是HttpRequest和HttpHeaders。一般來說要想從客戶端傳送http2訊息給支援http2的伺服器,那麼需要傳送這些http2Frame的物件,那麼可不可以像http1.1這樣傳送HttpRequest物件呢?

今天的文章將會給大家揭祕。

使用http1.1的方式處理http2

netty當然考慮到了客戶的這種需求,所以提供了兩個對應的類,分別是:InboundHttp2ToHttpAdapter和HttpToHttp2ConnectionHandler。

他們是一對方法,其中InboundHttp2ToHttpAdapter將接收到的HTTP/2 frames 轉換成為HTTP/1.x objects,而HttpToHttp2ConnectionHandler則是相反的將HTTP/1.x objects轉換成為HTTP/2 frames。 這樣我們在程式中只需要處理http1的物件即可。

他們的底層實際上呼叫了HttpConversionUtil類中的轉換方法,將HTTP2物件和HTTP1物件進行轉換。

處理TLS連線

和伺服器一樣,客戶端的連線也需要區分是TLS還是clear text,TLS簡單點,只需要處理HTTP2資料即可,clear text複雜點,需要考慮http升級的情況。

先看下TLS的連線處理。

首先是建立SslContext,客戶端的建立和伺服器端的建立沒什麼兩樣,這裡要注意的是SslContextBuilder呼叫的是forClient()方法:

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

然後將sslCtx的newHandler方法傳入到pipeline中:

pipeline.addLast(sslCtx.newHandler(ch.alloc(), CustHttp2Client.HOST, CustHttp2Client.PORT));

最後加入ApplicationProtocolNegotiationHandler,用於TLS擴充套件協議的協商:

pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
            @Override
            protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
                if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
                    ChannelPipeline p = ctx.pipeline();
                    p.addLast(connectionHandler);
                    p.addLast(settingsHandler, responseHandler);
                    return;
                }
                ctx.close();
                throw new IllegalStateException("未知協議: " + protocol);
            }
        });

如果是HTTP2協議,則需要向pipline中加入三個handler,分別是connectionHandler,settingsHandler和responseHandler。

connectionHandler用於處理客戶端和伺服器端的連線,這裡使用HttpToHttp2ConnectionHandlerBuilder來構建一個上一節提到的HttpToHttp2ConnectionHandler,用來將http1.1物件轉換成為http2物件。

Http2Connection connection = new DefaultHttp2Connection(false);
        connectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
                .frameListener(new DelegatingDecompressorFrameListener(
                        connection,
                        new InboundHttp2ToHttpAdapterBuilder(connection)
                                .maxContentLength(maxContentLength)
                                .propagateSettings(true)
                                .build()))
                .frameLogger(logger)
                .connection(connection)
                .build();

但是連線其實是雙向的,HttpToHttp2ConnectionHandler是將http1.1轉換成為http2,它實際上是一個outbound處理器,我們還需要一個inbound處理器,用來將接收到的http2物件轉換成為http1.1物件,這裡通過新增framelistener來實現。

frameListener傳入一個DelegatingDecompressorFrameListener,其內部又傳入了前一節介紹的InboundHttp2ToHttpAdapterBuilder用來對http2物件進行轉換。

settingsHandler用來處理Http2Settings inbound訊息,responseHandler用來處理FullHttpResponse inbound訊息。

這兩個是自定義的handler類。

處理h2c訊息

從上面的程式碼可以看出,我們在TLS的ProtocolNegotiation中只處理了HTTP2協議,如果是HTTP1協議,直接會報錯。如果是HTTP1協議,則可以通過clear text upgrade來實現,也就是h2c協議。

我們看下h2c需要新增的handler:

    private void configureClearText(SocketChannel ch) {
        HttpClientCodec sourceCodec = new HttpClientCodec();
        Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler);
        HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536);

        ch.pipeline().addLast(sourceCodec,
                              upgradeHandler,
                              new CustUpgradeRequestHandler(this),
                              new UserEventLogger());
    }

首先新增的是HttpClientCodec作為source編碼handler,然後新增HttpClientUpgradeHandler作為upgrade handler。最後新增自定義的CustUpgradeRequestHandler和事件記錄器UserEventLogger。

自定義的CustUpgradeRequestHandler負責在channelActive的時候,建立upgradeRequest併傳送到channel中。

因為upgradeCodec中已經包含了處理http2連線的connectionHandler,所以還需要手動新增settingsHandler和responseHandler。

ctx.pipeline().addLast(custHttp2ClientInitializer.settingsHandler(), custHttp2ClientInitializer.responseHandler());

傳送訊息

handler配置好了之後,我們就可以直接以http1的方式來傳送http2訊息了。

首先傳送一個get請求:

// 建立一個get請求
                FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, GETURL, Unpooled.EMPTY_BUFFER);
                request.headers().add(HttpHeaderNames.HOST, hostName);
                request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
                request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
                request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
                responseHandler.put(streamId, channel.write(request), channel.newPromise());

然後是一個post請求:

// 建立一個post請求
                FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, POSTURL,
                        wrappedBuffer(POSTDATA.getBytes(CharsetUtil.UTF_8)));
                request.headers().add(HttpHeaderNames.HOST, hostName);
                request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
                request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
                request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
                responseHandler.put(streamId, channel.write(request), channel.newPromise());

和普通的http1請求沒太大區別。

總結

通過使用InboundHttp2ToHttpAdapter和HttpToHttp2ConnectionHandler可以方便的使用http1的方法來傳送http2的訊息,非常方便。

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

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

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

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

相關文章