Использование SwiftNIO и SwiftNIOHTTP2 в качестве клиента HTTP2 - PullRequest
0 голосов
/ 25 ноября 2018

В настоящее время я работаю над простым клиентом HTTP2 в Swift, используя SwiftNIO и бета-версию SwiftNIOHTTP2.Моя реализация выглядит следующим образом:

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: удаленный узел возвратил неожиданные данные, пока мы ожидали фрейм SETTINGS.Возможно, 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)=[])

Как установитьсеанс и выдать запрос GET, используя SwiftNIOHTTP2?

1 Ответ

0 голосов
/ 25 ноября 2018

Это очень хороший вопрос!Давайте сначала проанализируем, почему это сложнее, чем отправка запроса HTTP / 1.x.В общих чертах, эти проблемы делятся на две категории:

  1. В настоящее время NIO усложняет, а не усложняет, поэтому многое из того, что я напишу ниже, порой может быть не интуитивным.Я являюсь одной из основных команд NIO, и даже мне пришлось покопаться в нем немного кода, чтобы все это работало, в основном потому, что у нас до сих пор нет генерации документов для swift-nio-ssl и swift-nio-http2 on http://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 (согласование протокола уровня приложения) , другой вариант - выполнить обновление HTTP / 1 до HTTP2, но это одновременно более сложный и менее производительный процесс, поэтому давайте не будем этого делать.здесь
  3. некоторые инструменты HTTP / 2: а) открыть новый HTTP / 2 б) перевод сообщений с HTTP / 2 на HTTP / 1 в) мультиплексирование HTTP / 2

код вВаш вопрос содержит самые важные биты, а именно 3b и 3c из приведенного выше списка.Но нам нужно добавить 1, 2 и 3a, поэтому давайте сделаем это:)

Давайте начнем с 2) ALPN:

let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)

Это конфигурация SSL с протоколом "h2" ALPNтам идентификатор, который сообщит серверу, что мы хотим говорить по HTTP / 2, как описано в спецификации 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)
    }
}

Хорошо, это готовые леса.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) -> 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, и тогда два партнера не понимали друг друга:).

...