swift - 使用 SwiftNIO 和 SwiftNIOHTTP2 作为 HTTP2 客户端

标签 swift http2 nghttp2 swift-nio

我目前正在使用 SwiftNIO 和 SwiftNIOHTTP2 测试版在 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) -> EventLoopFuture<Void> in
                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 error: Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.

但是,从命令行使用 nghttp2 连接和发出一个简单的请求工作正常。

$ nghttp -vn https://strnmn.me
[  0.048] Connected
The negotiated protocol: h2
[  0.110] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
          (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 <length=4, flags=0x00, stream_id=0>
          (window_size_increment=2147418112)
[  0.110] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.110] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.110] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.110] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.110] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.110] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.110] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.111] send HEADERS frame <length=35, flags=0x25, stream_id=13>
          ; 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 <length=0, flags=0x01, stream_id=0>
          ; 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 <length=185, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.142] recv DATA frame <length=114, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.142] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

如何使用 SwiftNIOHTTP2 建立 session 并发出 GET 请求?

最佳答案

这是个很好的问题!我们先分析一下为什么这比发送 HTTP/1.x 请求要复杂。从广义上讲,这些问题分为两类:

  1. NIO 目前使它变得比必要的更复杂,因此我接下来要写的很多内容有时可能不直观。我是 NIO 核心团队的一员,我什至不得不挖掘相当多的代码才能使其完全正常工作,主要是因为我们仍然没有 swift-nio-ssl 的文档生成。和 swift-nio-http2http://docs.swiftnio.io .
  2. HTTP/2 只是比 HTTP/1 复杂得多,而 NIO 更像是一个可用于构建 HTTP 客户端的工具箱,因此我们需要结合使用大量工具才能使其全部正常工作。

我将在这里重点介绍必要的复杂性 (2),并将提交 (1) 的错误/修复。让我们检查一下我们需要 NIO 工具箱中的哪些工具来实现这一点:

  1. TLS。没有任何真实世界的 HTTP/2 服务器会允许您通过明文传输 HTTP/2
  2. ALPN。 HTTP/1 和 HTTP/2 共享同一个端口(通常是 443),所以我们需要告诉服务器我们想要使用 HTTP/2,因为为了向后兼容,默认仍然是 HTTP/1。我们可以使用一种称为 ALPN (Application-layer Protocol Negotiation) 的机制来做到这一点,另一种选择是将 HTTP/1 升级到 HTTP2,但这更复杂且性能更差,所以我们不要在这里这样做
  3. 一些 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 协议(protocol)标识符的 SSL 配置,它将告诉服务器我们想要使用 HTTP/2,如 HTTP/2 spec 中所述。 .

好的,让我们使用之前设置的 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<Void> {
            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 流成功打开,就会添加该处理程序。为了查看完整的响应,我还实现了将响应的所有位累加到一个 promise 中:

/// 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)
        }
    }
}

为了完成它,让我们设置客户端的 channel 管道:

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) -> EventLoopFuture<Void> in
            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 向远程端传输纯文本 HTTP/2,远程端传输 TLS,然后两个对等点无法相互理解:)。

关于swift - 使用 SwiftNIO 和 SwiftNIOHTTP2 作为 HTTP2 客户端,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53466997/

相关文章:

swift - 检查 Array as Any 是否存在 Swift 中的元素

ios - Swift:如何以编程方式获取 iOS 设备的唯一标识符?

java - 如何配置 Netty 4 HTTP/2 客户端来获取单个帧

在 nghttp2 示例中看不到 Http-Settings header

cURL 不适用于 nghttp2

google-chrome - 我们应该在 HTTP/2 中复用多少并发请求

ios - 暂停 TableView 滚动

ios - 核心数据查询值 "is not nil"未返回值等于 0 的对象

server - HTTP/2 服务器推送和浏览器缓存

okhttp - 如何验证 okhttp 是否使用 http/2 进行请求?