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


簡介

對於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/

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM