Using SwiftNIO and SwiftNIOHTTP2 as an HTTP2 client
I'm currently working on a simple HTTP2 client in Swift using SwiftNIO and the SwiftNIOHTTP2 beta.
My implementation looks like this:
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()
Unfortunately the connection always fails with an error:
nghttp2 error: Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
However, connecting and issuing a simple request using nghttp2 from the command line works fine.
$ 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)=)
How can I establish a session and issue a GET request using SwiftNIOHTTP2?
swift http2 nghttp2 swift-nio
add a comment |
I'm currently working on a simple HTTP2 client in Swift using SwiftNIO and the SwiftNIOHTTP2 beta.
My implementation looks like this:
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()
Unfortunately the connection always fails with an error:
nghttp2 error: Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
However, connecting and issuing a simple request using nghttp2 from the command line works fine.
$ 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)=)
How can I establish a session and issue a GET request using SwiftNIOHTTP2?
swift http2 nghttp2 swift-nio
add a comment |
I'm currently working on a simple HTTP2 client in Swift using SwiftNIO and the SwiftNIOHTTP2 beta.
My implementation looks like this:
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()
Unfortunately the connection always fails with an error:
nghttp2 error: Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
However, connecting and issuing a simple request using nghttp2 from the command line works fine.
$ 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)=)
How can I establish a session and issue a GET request using SwiftNIOHTTP2?
swift http2 nghttp2 swift-nio
I'm currently working on a simple HTTP2 client in Swift using SwiftNIO and the SwiftNIOHTTP2 beta.
My implementation looks like this:
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()
Unfortunately the connection always fails with an error:
nghttp2 error: Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
However, connecting and issuing a simple request using nghttp2 from the command line works fine.
$ 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)=)
How can I establish a session and issue a GET request using SwiftNIOHTTP2?
swift http2 nghttp2 swift-nio
swift http2 nghttp2 swift-nio
asked Nov 25 '18 at 11:31
iMoritziMoritz
166214
166214
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
That's a very good question! Let's first analyse why this is more complicated than sending a HTTP/1.x request. Broadly speaking these issues fall into two categories:
- NIO at the moment makes it more complicated than necessary, therefore much of what I'll write further down can be unintuitive at times. I'm one of the NIO core team and even I had to dig through quite a bit of code to get this fully working, mostly because we still don't have doc generation for
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io . - HTTP/2 is just much more complicated than HTTP/1 and NIO is more a toolbox that can be used to build HTTP clients so we need to use a bunch of tools together to get it all working.
I'll focus on the necessary complexity (2) here and will file bugs/fixes for (1). Let's check what tools we need from the NIO toolbox to get this working:
- TLS. No real-world HTTP/2 server will allow you to speak HTTP/2 over plaintext
- ALPN. HTTP/1 and HTTP/2 share the same port (usually
443
) so we need to tell the server that we want to speak HTTP/2 because for backwards compatibility the default remains HTTP/1. We can do this using a mechanism called ALPN (Application-layer Protocol Negotiation), the other option would be to perform a HTTP/1 upgrade to HTTP2 but that's both more complicated and less performant so let's not do this here - some HTTP/2 tools: a) open a new HTTP/2 b) HTTP/2 to HTTP/1 message translation c) HTTP/2 multiplexing
The code in your question contains the most important bits, namely 3b and 3c of the above list. But we need to add 1, 2 and 3a so let's do this :)
Let's start with 2) ALPN:
let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)
This is an SSL configuration with the "h2"
ALPN protocol identifier there which will tell the server that we want to speak HTTP/2 as documented in the HTTP/2 spec.
Ok, let's add TLS with the sslContext
set up before:
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
It's also important that we tell the OpenSSLClientHandler
the server's hostname so it can validate the certificate properly.
Lastly we need to do 3a (creating a new HTTP/2 stream to issue our request on) which can be easily done using a 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)
}
}
Okay, that's the scaffolding done. The SendAGETRequestHandler
is the last part which is a handler that will be added as soon as the new HTTP/2 stream that we have opened before has been opened successfully. To see the full response, I also implemented accumulating all bits of the response together into a 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)
}
}
}
To finish it up, let's set up the client's channel pipeline:
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 {
}
}
To see a fully working example, I put something together a PR for swift-nio-examples/http2-client
.
Oh, and the reason that NIO was claiming that the other end isn't speaking HTTP/2 properly was the lack of TLS. There was no OpenSSLHandler
so NIO was speaking plaintext HTTP/2 to a remote end which was speaking TLS and then the two peers don't understand each other :).
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53466997%2fusing-swiftnio-and-swiftniohttp2-as-an-http2-client%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
That's a very good question! Let's first analyse why this is more complicated than sending a HTTP/1.x request. Broadly speaking these issues fall into two categories:
- NIO at the moment makes it more complicated than necessary, therefore much of what I'll write further down can be unintuitive at times. I'm one of the NIO core team and even I had to dig through quite a bit of code to get this fully working, mostly because we still don't have doc generation for
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io . - HTTP/2 is just much more complicated than HTTP/1 and NIO is more a toolbox that can be used to build HTTP clients so we need to use a bunch of tools together to get it all working.
I'll focus on the necessary complexity (2) here and will file bugs/fixes for (1). Let's check what tools we need from the NIO toolbox to get this working:
- TLS. No real-world HTTP/2 server will allow you to speak HTTP/2 over plaintext
- ALPN. HTTP/1 and HTTP/2 share the same port (usually
443
) so we need to tell the server that we want to speak HTTP/2 because for backwards compatibility the default remains HTTP/1. We can do this using a mechanism called ALPN (Application-layer Protocol Negotiation), the other option would be to perform a HTTP/1 upgrade to HTTP2 but that's both more complicated and less performant so let's not do this here - some HTTP/2 tools: a) open a new HTTP/2 b) HTTP/2 to HTTP/1 message translation c) HTTP/2 multiplexing
The code in your question contains the most important bits, namely 3b and 3c of the above list. But we need to add 1, 2 and 3a so let's do this :)
Let's start with 2) ALPN:
let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)
This is an SSL configuration with the "h2"
ALPN protocol identifier there which will tell the server that we want to speak HTTP/2 as documented in the HTTP/2 spec.
Ok, let's add TLS with the sslContext
set up before:
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
It's also important that we tell the OpenSSLClientHandler
the server's hostname so it can validate the certificate properly.
Lastly we need to do 3a (creating a new HTTP/2 stream to issue our request on) which can be easily done using a 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)
}
}
Okay, that's the scaffolding done. The SendAGETRequestHandler
is the last part which is a handler that will be added as soon as the new HTTP/2 stream that we have opened before has been opened successfully. To see the full response, I also implemented accumulating all bits of the response together into a 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)
}
}
}
To finish it up, let's set up the client's channel pipeline:
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 {
}
}
To see a fully working example, I put something together a PR for swift-nio-examples/http2-client
.
Oh, and the reason that NIO was claiming that the other end isn't speaking HTTP/2 properly was the lack of TLS. There was no OpenSSLHandler
so NIO was speaking plaintext HTTP/2 to a remote end which was speaking TLS and then the two peers don't understand each other :).
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
add a comment |
That's a very good question! Let's first analyse why this is more complicated than sending a HTTP/1.x request. Broadly speaking these issues fall into two categories:
- NIO at the moment makes it more complicated than necessary, therefore much of what I'll write further down can be unintuitive at times. I'm one of the NIO core team and even I had to dig through quite a bit of code to get this fully working, mostly because we still don't have doc generation for
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io . - HTTP/2 is just much more complicated than HTTP/1 and NIO is more a toolbox that can be used to build HTTP clients so we need to use a bunch of tools together to get it all working.
I'll focus on the necessary complexity (2) here and will file bugs/fixes for (1). Let's check what tools we need from the NIO toolbox to get this working:
- TLS. No real-world HTTP/2 server will allow you to speak HTTP/2 over plaintext
- ALPN. HTTP/1 and HTTP/2 share the same port (usually
443
) so we need to tell the server that we want to speak HTTP/2 because for backwards compatibility the default remains HTTP/1. We can do this using a mechanism called ALPN (Application-layer Protocol Negotiation), the other option would be to perform a HTTP/1 upgrade to HTTP2 but that's both more complicated and less performant so let's not do this here - some HTTP/2 tools: a) open a new HTTP/2 b) HTTP/2 to HTTP/1 message translation c) HTTP/2 multiplexing
The code in your question contains the most important bits, namely 3b and 3c of the above list. But we need to add 1, 2 and 3a so let's do this :)
Let's start with 2) ALPN:
let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)
This is an SSL configuration with the "h2"
ALPN protocol identifier there which will tell the server that we want to speak HTTP/2 as documented in the HTTP/2 spec.
Ok, let's add TLS with the sslContext
set up before:
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
It's also important that we tell the OpenSSLClientHandler
the server's hostname so it can validate the certificate properly.
Lastly we need to do 3a (creating a new HTTP/2 stream to issue our request on) which can be easily done using a 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)
}
}
Okay, that's the scaffolding done. The SendAGETRequestHandler
is the last part which is a handler that will be added as soon as the new HTTP/2 stream that we have opened before has been opened successfully. To see the full response, I also implemented accumulating all bits of the response together into a 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)
}
}
}
To finish it up, let's set up the client's channel pipeline:
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 {
}
}
To see a fully working example, I put something together a PR for swift-nio-examples/http2-client
.
Oh, and the reason that NIO was claiming that the other end isn't speaking HTTP/2 properly was the lack of TLS. There was no OpenSSLHandler
so NIO was speaking plaintext HTTP/2 to a remote end which was speaking TLS and then the two peers don't understand each other :).
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
add a comment |
That's a very good question! Let's first analyse why this is more complicated than sending a HTTP/1.x request. Broadly speaking these issues fall into two categories:
- NIO at the moment makes it more complicated than necessary, therefore much of what I'll write further down can be unintuitive at times. I'm one of the NIO core team and even I had to dig through quite a bit of code to get this fully working, mostly because we still don't have doc generation for
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io . - HTTP/2 is just much more complicated than HTTP/1 and NIO is more a toolbox that can be used to build HTTP clients so we need to use a bunch of tools together to get it all working.
I'll focus on the necessary complexity (2) here and will file bugs/fixes for (1). Let's check what tools we need from the NIO toolbox to get this working:
- TLS. No real-world HTTP/2 server will allow you to speak HTTP/2 over plaintext
- ALPN. HTTP/1 and HTTP/2 share the same port (usually
443
) so we need to tell the server that we want to speak HTTP/2 because for backwards compatibility the default remains HTTP/1. We can do this using a mechanism called ALPN (Application-layer Protocol Negotiation), the other option would be to perform a HTTP/1 upgrade to HTTP2 but that's both more complicated and less performant so let's not do this here - some HTTP/2 tools: a) open a new HTTP/2 b) HTTP/2 to HTTP/1 message translation c) HTTP/2 multiplexing
The code in your question contains the most important bits, namely 3b and 3c of the above list. But we need to add 1, 2 and 3a so let's do this :)
Let's start with 2) ALPN:
let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)
This is an SSL configuration with the "h2"
ALPN protocol identifier there which will tell the server that we want to speak HTTP/2 as documented in the HTTP/2 spec.
Ok, let's add TLS with the sslContext
set up before:
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
It's also important that we tell the OpenSSLClientHandler
the server's hostname so it can validate the certificate properly.
Lastly we need to do 3a (creating a new HTTP/2 stream to issue our request on) which can be easily done using a 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)
}
}
Okay, that's the scaffolding done. The SendAGETRequestHandler
is the last part which is a handler that will be added as soon as the new HTTP/2 stream that we have opened before has been opened successfully. To see the full response, I also implemented accumulating all bits of the response together into a 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)
}
}
}
To finish it up, let's set up the client's channel pipeline:
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 {
}
}
To see a fully working example, I put something together a PR for swift-nio-examples/http2-client
.
Oh, and the reason that NIO was claiming that the other end isn't speaking HTTP/2 properly was the lack of TLS. There was no OpenSSLHandler
so NIO was speaking plaintext HTTP/2 to a remote end which was speaking TLS and then the two peers don't understand each other :).
That's a very good question! Let's first analyse why this is more complicated than sending a HTTP/1.x request. Broadly speaking these issues fall into two categories:
- NIO at the moment makes it more complicated than necessary, therefore much of what I'll write further down can be unintuitive at times. I'm one of the NIO core team and even I had to dig through quite a bit of code to get this fully working, mostly because we still don't have doc generation for
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io . - HTTP/2 is just much more complicated than HTTP/1 and NIO is more a toolbox that can be used to build HTTP clients so we need to use a bunch of tools together to get it all working.
I'll focus on the necessary complexity (2) here and will file bugs/fixes for (1). Let's check what tools we need from the NIO toolbox to get this working:
- TLS. No real-world HTTP/2 server will allow you to speak HTTP/2 over plaintext
- ALPN. HTTP/1 and HTTP/2 share the same port (usually
443
) so we need to tell the server that we want to speak HTTP/2 because for backwards compatibility the default remains HTTP/1. We can do this using a mechanism called ALPN (Application-layer Protocol Negotiation), the other option would be to perform a HTTP/1 upgrade to HTTP2 but that's both more complicated and less performant so let's not do this here - some HTTP/2 tools: a) open a new HTTP/2 b) HTTP/2 to HTTP/1 message translation c) HTTP/2 multiplexing
The code in your question contains the most important bits, namely 3b and 3c of the above list. But we need to add 1, 2 and 3a so let's do this :)
Let's start with 2) ALPN:
let tlsConfig = TLSConfiguration.forClient(applicationProtocols: ["h2"])
let sslContext = try SSLContext(configuration: tlsConfig)
This is an SSL configuration with the "h2"
ALPN protocol identifier there which will tell the server that we want to speak HTTP/2 as documented in the HTTP/2 spec.
Ok, let's add TLS with the sslContext
set up before:
let sslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: hostname)
It's also important that we tell the OpenSSLClientHandler
the server's hostname so it can validate the certificate properly.
Lastly we need to do 3a (creating a new HTTP/2 stream to issue our request on) which can be easily done using a 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)
}
}
Okay, that's the scaffolding done. The SendAGETRequestHandler
is the last part which is a handler that will be added as soon as the new HTTP/2 stream that we have opened before has been opened successfully. To see the full response, I also implemented accumulating all bits of the response together into a 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)
}
}
}
To finish it up, let's set up the client's channel pipeline:
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 {
}
}
To see a fully working example, I put something together a PR for swift-nio-examples/http2-client
.
Oh, and the reason that NIO was claiming that the other end isn't speaking HTTP/2 properly was the lack of TLS. There was no OpenSSLHandler
so NIO was speaking plaintext HTTP/2 to a remote end which was speaking TLS and then the two peers don't understand each other :).
edited Nov 25 '18 at 15:44
answered Nov 25 '18 at 14:59
Johannes WeissJohannes Weiss
39.4k1483120
39.4k1483120
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
add a comment |
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
Thank you for the awesome explanation! I just got started with SwiftNIO and this helps me a lot :)
– iMoritz
Nov 26 '18 at 18:20
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53466997%2fusing-swiftnio-and-swiftniohttp2-as-an-http2-client%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown