Использование SwiftNIO и SwiftNIOHTTP2 в качестве клиента HTTP2
В настоящее время я работаю над простым клиентом 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: удаленный узел возвратил неожиданные данные, пока мы ожидали фрейм 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 ответ
Это очень хороший вопрос! Давайте сначала проанализируем, почему это сложнее, чем отправка запроса 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 (согласование протокола прикладного уровня), другой вариант - выполнить обновление HTTP / 1 до HTTP2, но это более сложный и менее производительный процесс, поэтому давайте не будем делать это здесь. - некоторые инструменты HTTP/2: а) открыть новый HTTP/2; б) перевод HTTP/2 в HTTP / 1; в) мультиплексирование HTTP/2;
Код в вашем вопросе содержит самые важные биты, а именно 3b и 3c из приведенного выше списка. Но нам нужно добавить 1, 2 и 3а, так что давайте сделаем это:)
Давайте начнем с 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 {
}
}
Чтобы увидеть полностью рабочий пример, я собрал что-то пиар для swift-nio-examples/http2-client
,
О, и причиной того, что NIO заявлял, что другой конец не говорит по-HTTP/2, было отсутствие TLS. Не было OpenSSLHandler
поэтому NIO передавал открытый текст HTTP/2 удаленной стороне, которая говорила по TLS, и тогда два партнера не понимают друг друга:).