天天看點

netty系列之:搭建用戶端使用http1.1的方式連接配接http2伺服器簡介使用http1.1的方式處理http2處理TLS連接配接處理h2c消息發送消息總結

簡介

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

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

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

繼續閱讀