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