簡介
對于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/最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!