我目前正在使用SwiftNIO和SwiftNIOHTTP2 beta在Swift中使用简单的HTTP2客户端.我的实现看起来像这样:
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) let bootstrap = ClientBootstrap(group: group) .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .channelInitializer { channel in channel.pipeline.add(handler: HTTP2Parser(mode: .client)).then { let multiplexer = HTTP2StreamMultiplexer { (channel, streamID) -> EventLoopFuturein return channel.pipeline.add(handler: HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: .https)) } return channel.pipeline.add(handler: multiplexer) } } defer { try! group.syncShutdownGracefully() } let url = URL(string: "https://strnmn.me")! _ = try bootstrap.connect(host: url.host!, port: url.port ?? 443) .wait()
不幸的是,连接始终失败并出现错误:
nghttp2错误:当我们期望SETTINGS帧时,远程对等体返回了意外数据.也许,peer不能正确支持HTTP/2.
但是,从命令行使用nghttp2连接和发出一个简单的请求可以正常工作.
$ nghttp -vn https://strnmn.me [ 0.048] Connected The negotiated protocol: h2 [ 0.110] recv SETTINGS frame(niv=3) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):128] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65536] [SETTINGS_MAX_FRAME_SIZE(0x05):16777215] [ 0.110] recv WINDOW_UPDATE frame (window_size_increment=2147418112) [ 0.110] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.110] send SETTINGS frame ; ACK (niv=0) [ 0.110] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.110] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.110] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.110] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.110] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.111] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: https :authority: strnmn.me accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.34.0 [ 0.141] recv SETTINGS frame ; ACK (niv=0) [ 0.141] recv (stream_id=13) :status: 200 [ 0.141] recv (stream_id=13) server: nginx [ 0.141] recv (stream_id=13) date: Sat, 24 Nov 2018 16:29:13 GMT [ 0.141] recv (stream_id=13) content-type: text/html [ 0.141] recv (stream_id=13) last-modified: Sat, 01 Jul 2017 20:23:11 GMT [ 0.141] recv (stream_id=13) vary: Accept-Encoding [ 0.141] recv (stream_id=13) etag: W/"595804af-8a" [ 0.141] recv (stream_id=13) expires: Sat, 24 Nov 2018 16:39:13 GMT [ 0.141] recv (stream_id=13) cache-control: max-age=600 [ 0.141] recv (stream_id=13) x-frame-options: SAMEORIGIN [ 0.141] recv (stream_id=13) content-encoding: gzip [ 0.141] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header [ 0.142] recv DATA frame ; END_STREAM [ 0.142] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
如何使用SwiftNIOHTTP2建立会话并发出GET请求?
这是一个非常好的问题!让我们首先分析为什么这比发送HTTP/1.x请求更复杂.从广义上讲,这些问题分为两类:
NIO目前使它变得比必要的更复杂,因此我进一步写下来的很多东西有时候会不直观.我是NIO核心团队之一,甚至我有过相当多的代码来挖得到这个完全正常的,主要是因为我们还没有文档的一代swift-nio-ssl
和swift-nio-http2
上http://docs.swiftnio.io.
HTTP/2比HTTP/1复杂得多,而NIO更像是一个可用于构建HTTP客户端的工具箱,因此我们需要一起使用一堆工具来使其全部工作.
我将专注于必要的复杂性(2),并将为(1)提交错误/修复.让我们从NIO工具箱中检查我们需要哪些工具来实现这一点:
TLS.没有真实的HTTP/2服务器允许您通过纯文本说HTTP/2
ALPN.HTTP/1和HTTP/2共享相同的端口(通常443
),因此我们需要告诉服务器我们想要说HTTP/2,因为为了向后兼容,默认值仍然是HTTP/1.我们可以使用一种称为ALPN(应用层协议协商)的机制来实现这一点,另一种选择是对HTTP2执行HTTP/1升级,但这更复杂,性能更低,所以我们不要在这里做
一些HTTP/2工具:a)打开一个新的HTTP/2 b)HTTP/2到HTTP/1消息转换c)HTTP/2多路复用
您问题中的代码包含最重要的位,即上面列表中的3b和3c.但是我们需要添加1,2和3a所以让我们这样做:)
让我们从2)ALPN开始:
let tlsCOnfig= TLSConfiguration.forClient(applicationProtocols: ["h2"]) let sslCOntext= try SSLContext(configuration: tlsConfig)
这是一个带有"h2"
ALPN协议标识符的SSL配置,它将告诉服务器我们想要说HTTP/2规范中记录的HTTP/2.
好的,让我们sslContext
在之前添加TLS :
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
我们告诉OpenSSLClientHandler
服务器的主机名以便它可以正确验证证书也很重要.
最后,我们需要做3a(创建一个新的HTTP/2流来发出我们的请求),这可以通过以下方式轻松完成ChannelHandler
:
/// Creates a new HTTP/2 stream when our channel is active and adds the `SendAGETRequestHandler` so a request is sent. final class CreateRequestStreamHandler: ChannelInboundHandler { typealias InboundIn = Never private let multiplexer: HTTP2StreamMultiplexer private let responseReceivedPromise: EventLoopPromise<[HTTPClientResponsePart]> init(multiplexer: HTTP2StreamMultiplexer, responseReceivedPromise: EventLoopPromise<[HTTPClientResponsePart]>) { self.multiplexer = multiplexer self.respOnseReceivedPromise= responseReceivedPromise } func channelActive(ctx: ChannelHandlerContext) { func requestStreamInitializer(channel: Channel, streamID: HTTP2StreamID) -> EventLoopFuture{ return channel.pipeline.addHandlers([HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: .https), SendAGETRequestHandler(responseReceivedPromise: self.responseReceivedPromise)], first: false) } // this is the most important line: When the channel is active we add the `HTTP2ToHTTP1ClientCodec` to deal in HTTP/1 messages as well as the `SendAGETRequestHandler` which will send a request. self.multiplexer.createStreamChannel(promise: nil, requestStreamInitializer) } }
好的,那是脚手架完成的.该SendAGETRequestHandler
是这是将要尽快,我们之前打开新的HTTP/2流已经成功打开添加的处理程序的最后一部分.为了看到完整的响应,我还实现了将响应的所有位累加到一个承诺中:
/// Fires off a GET request when our stream is active and collects all response parts into a promise. /// /// - warning: This will read the whole response into memory and delivers it into a promise. final class SendAGETRequestHandler: ChannelInboundHandler { typealias InboundIn = HTTPClientResponsePart typealias OutboundOut = HTTPClientRequestPart private let responseReceivedPromise: EventLoopPromise<[HTTPClientResponsePart]> private var responsePartAccumulator: [HTTPClientResponsePart] = [] init(responseReceivedPromise: EventLoopPromise<[HTTPClientResponsePart]>) { self.respOnseReceivedPromise= responseReceivedPromise } func channelActive(ctx: ChannelHandlerContext) { assert(ctx.channel.parent!.isActive) var reqHead = HTTPRequestHead(version: .init(major: 2, minor: 0), method: .GET, uri: "/") reqHead.headers.add(name: "Host", value: hostname) ctx.write(self.wrapOutboundOut(.head(reqHead)), promise: nil) ctx.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) } func channelRead(ctx: ChannelHandlerContext, data: NIOAny) { let resPart = self.unwrapInboundIn(data) self.responsePartAccumulator.append(resPart) if case .end = resPart { self.responseReceivedPromise.succeed(result: self.responsePartAccumulator) } } }
为了完成它,让我们设置客户端的渠道管道:
let bootstrap = ClientBootstrap(group: group) .channelInitializer { channel in let myEventLoop = channel.eventLoop let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname) let http2Parser = HTTP2Parser(mode: .client) let http2Multiplexer = HTTP2StreamMultiplexer { (channel, streamID) -> EventLoopFuturein return myEventLoop.newSucceededFuture(result: ()) } return channel.pipeline.addHandlers([sslHandler, http2Parser, http2Multiplexer, CreateRequestStreamHandler(multiplexer: http2Multiplexer, responseReceivedPromise: responseReceivedPromise), CollectErrorsAndCloseStreamHandler(responseReceivedPromise: responseReceivedPromise)], first: false).map { } }
为了看到一个完整的例子,我把一些东西放在一起PR swift-nio-examples/http2-client
.
哦,NIO声称另一端没有正确说HTTP/2的原因是缺乏TLS.没有OpenSSLHandler
那么NIO正在讲一个说明TLS的远程端的明文HTTP/2,然后这两个对等人彼此不理解:).