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?
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:
swift-nio-ssl
andswift-nio-http2
on http://docs.swiftnio.io .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:
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 hereThe 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:
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: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
: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:To finish it up, let's set up the client's channel pipeline:
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 :).