使用SwiftNIO和SwiftNIOHTTP2作为HTTP2客户端

使用SwiftNIO和SwiftNIOHTTP2作为HTTP2客户端,swift,http2,nghttp2,swift-nio,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_REU

我目前正在使用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()
let group=multi-threadedeventloopgroup(线程数:1)
let bootstrap=ClientBootstrap(组:group)
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL\u socket),SO\u REUSEADDR),值:1)
.channelInitializer{channel in
channel.pipeline.add(处理程序:HTTP2Parser(模式:.client))。然后{
让多路复用器=HTTP2StreamMultiplexer{(通道,streamID)->未来的事件循环
返回channel.pipeline.add(处理程序:http2tohtp1clientcodec(streamID:streamID,httpProtocol:.https))
}
返回channel.pipeline.add(处理程序:多路复用器)
}
}
推迟{
试试!group.syncShutdownGracefully()
}
让url=url(字符串:https://strnmn.me")!
_=尝试引导.connect(主机:url.host!,端口:url.port??443)
.等等
不幸的是,连接总是失败并出现错误:

nghttp2错误:当我们需要设置帧时,远程对等方返回了意外数据。也许,peer不正确地支持HTTP/2

但是,从命令行使用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)=[])
$nghttp-vnhttps://strnmn.me
[0.048]已连接
谈判达成的议定书:h2
[0.110]recv设置帧
(niv=3)
[设置\u最大\u并发\u流(0x03):128]
[设置\初始\窗口\大小(0x04):65536]
[设置最大帧大小(0x05):16777215]
[0.110]记录窗口\更新帧
(窗口大小增量=2147418112)
[0.110]发送设置帧
(niv=2)
[设置\u最大\u并发\u流(0x03):100]
[设置\初始\窗口大小(0x04):65535]
[0.110]发送设置帧
; 阿克
(niv=0)
[0.110]发送优先级帧
(dep_stream_id=0,weight=201,exclusive=0)
[0.110]发送优先级帧
(dep_stream_id=0,weight=101,exclusive=0)
[0.110]发送优先级帧
(dep_stream_id=0,weight=1,exclusive=0)
[0.110]发送优先级帧
(dep_stream_id=7,weight=1,exclusive=0)
[0.110]发送优先级帧
(dep_stream_id=3,weight=1,exclusive=0)
[0.111]发送标题帧
; 结束流|结束头|优先级
(padlen=0,dep\u stream\u id=11,weight=16,exclusive=0)
; 开源节流
:方法:获取
:路径:/
:scheme:https
:权威:strnmn.me
接受:*/*
接受编码:gzip,deflate
用户代理:nghttp2/1.34.0
[0.141]recv设置帧
; 阿克
(niv=0)
[0.141]recv(流id=13):状态:200
[0.141]recv(stream_id=13)服务器:nginx
[0.141]recv(stream_id=13)日期:2018年11月24日星期六格林威治标准时间16:29:13
[0.141]recv(stream_id=13)内容类型:text/html
[0.141]recv(stream_id=13)最后修改时间:2017年7月1日星期六20:23:11 GMT
[0.141]recv(流id=13)变化:接受编码
[0.141]recv(stream_id=13)etag:W/“595804af-8a”
[0.141]recv(stream_id=13)到期时间:2018年11月24日星期六格林威治标准时间16:39:13
[0.141]recv(流id=13)缓存控制:最大年龄=600
[0.141]recv(stream_id=13)x帧选项:SAMEORIGIN
[0.141]recv(stream_id=13)内容编码:gzip
[0.141]接收头帧
; 端头
(padlen=0)
; 第一响应头
[0.142]recv数据帧
; 尾流
[0.142]发送球门框架
(最后一个\u流\u id=0,错误\u代码=无错误(0x00),不透明\u数据(0)=[])

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

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

  • NIO目前使它变得比必要的更复杂,因此我将进一步写下的许多内容有时可能是不直观的。我是NIO核心团队中的一员,甚至我也必须钻研相当多的代码才能让它完全工作,主要是因为我们仍然没有为和生成文档
  • HTTP/2比HTTP/1复杂得多,NIO更像是一个工具箱,可以用来构建HTTP客户机,因此我们需要使用一系列工具来实现这一切
  • 我将在这里重点讨论必要的复杂性(2),并将为(1)提交错误/修复。让我们检查一下NIO工具箱中需要哪些工具来实现这一点:

  • TLS。任何真实的HTTP/2服务器都不允许您通过明文说HTTP/2
  • 阿尔卑斯山。HTTP/1和HTTP/2共享同一个端口(通常为
    443
    ),因此我们需要告诉服务器我们想要说HTTP/2,因为为了向后兼容,默认值仍然是HTTP/1。我们可以使用一个名为的机制来实现这一点,另一个选项是执行HTTP/1升级到HTTP2,但这更复杂,性能也更低,所以我们不要在这里这样做
  • 一些HTTP/2工具:a)打开新的HTTP/2 b)HTTP/2到HTTP/1消息转换c)HTTP/2多路复用
  • 您问题中的代码包含最重要的位,即上面列表中的3b和3c。但我们需要添加1、2和3a,所以让我们这样做:)

    让我们从阿尔卑斯山开始:

    这是一个SSL配置,其中包含
    “h2”
    ALPN协议标识符,它将告诉服务器我们想要使用HTTP/2,如中所述

    好的,让我们添加TLS,并在其前面设置
    sslContext

    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)
        }
    }
    
    要完成它,让我们设置客户端的通道管道:

    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 {
    
            }
    }
    
    let bootstrap=ClientBootstrap(组:组)
    
    /// 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) -> 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 {
    
            }
    }