-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
full example demonstrating the universal bootstrap
- Loading branch information
Showing
5 changed files
with
417 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// swift-tools-version:5.1 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "UniversalBootstrapDemo", | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.16.1"), | ||
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.7.1"), | ||
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"), | ||
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.5")) | ||
], | ||
targets: [ | ||
.target( | ||
name: "UniversalBootstrapDemo", | ||
dependencies: ["NIO", "NIOSSL", "NIOTransportServices", "NIOHTTP1", "ArgumentParser"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# UniversalBootstrapDemo | ||
|
||
This little package demonstrates how you can use SwiftNIO's universal bootstraps. That allows you to fully support Network.framework on | ||
Apple platforms (if new enough) as well as BSD Sockets on Linux (and older Apple platforms). | ||
|
||
## Examples | ||
|
||
### Platform best | ||
|
||
To use the best networking available on your platform, try | ||
|
||
swift run UniversalBootstrapDemo https://httpbin.org/get | ||
|
||
The output would be for example: | ||
|
||
``` | ||
# Channel | ||
NIOTransportServices.NIOTSConnectionChannel | ||
``` | ||
|
||
Ah, we're running on a `NIOTSConnectionChannel` which means Network.framework was used to provide the underlying TCP connection. | ||
|
||
|
||
``` | ||
# ChannelPipeline | ||
ChannelPipeline[ObjectIdentifier(0x00007fae44e06c80)]: | ||
[I] ↓↑ [O] | ||
↓↑ HTTPRequestEncoder [handler0] | ||
ByteToMessageHandler<HTTPDecoder<HTTPPart<HTTPResponseHead, ByteBuffer>, HTTPPart<HTTPRequestHead, IOData>>> ↓↑ ByteToMessageHandler<HTTPDecoder<HTTPPart<HTTPResponseHead, ByteBuffer>, HTTPPart<HTTPRequestHead, IOData>>> [handler1] | ||
PrintToStdoutHandler ↓↑ [handler2] | ||
``` | ||
|
||
Note, that there is no `NIOSSLClientHandler` in the pipeline despite using HTTPS. That is because Network.framework does also providing | ||
the TLS support. | ||
|
||
``` | ||
# HTTP response body | ||
{ | ||
"args": {}, | ||
"headers": { | ||
"Host": "httpbin.org", | ||
"X-Amzn-Trace-Id": "Root=1-5eb1a4aa-4004f9686506e319aebd44a1" | ||
}, | ||
"origin": "86.158.121.11", | ||
"url": "https://httpbin.org/get" | ||
} | ||
``` | ||
|
||
### Running with an `EventLoopGroup` selected by somebody else | ||
|
||
To imitate your library needing to support an `EventLoopGroup` of unknown backing that was passed in from a client, you may want to try | ||
|
||
swift run UniversalBootstrapDemo --force-bsd-sockets https://httpbin.org/get | ||
|
||
The new output is now | ||
|
||
``` | ||
# Channel | ||
SocketChannel { BaseSocket { fd=9 }, active = true, localAddress = Optional([IPv4]192.168.4.26/192.168.4.26:60266), remoteAddress = Optional([IPv4]35.170.216.115/35.170.216.115:443) } | ||
``` | ||
|
||
Which uses BSD sockets. | ||
|
||
``` | ||
# ChannelPipeline | ||
ChannelPipeline[ObjectIdentifier(0x00007fb7b0609c40)]: | ||
[I] ↓↑ [O] | ||
NIOSSLClientHandler ↓↑ NIOSSLClientHandler [handler3] | ||
↓↑ HTTPRequestEncoder [handler0] | ||
ByteToMessageHandler<HTTPDecoder<HTTPPart<HTTPResponseHead, ByteBuffer>, HTTPPart<HTTPRequestHead, IOData>>> ↓↑ ByteToMessageHandler<HTTPDecoder<HTTPPart<HTTPResponseHead, ByteBuffer>, HTTPPart<HTTPRequestHead, IOData>>> [handler1] | ||
PrintToStdoutHandler ↓↑ [handler2] | ||
``` | ||
|
||
And the `ChannelPipeline` now also contains the `NIOSSLClientHandler` because SwiftNIOSSL now has to take care of TLS encryption. | ||
|
||
``` | ||
# HTTP response body | ||
{ | ||
"args": {}, | ||
"headers": { | ||
"Host": "httpbin.org", | ||
"X-Amzn-Trace-Id": "Root=1-5eb1a543-8fcbadf00a2b9990969c35c0" | ||
}, | ||
"origin": "86.158.121.11", | ||
"url": "https://httpbin.org/get" | ||
} | ||
``` |
159 changes: 159 additions & 0 deletions
159
UniversalBootstrapDemo/Sources/UniversalBootstrapDemo/EventLoopGroupManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import NIO | ||
import NIOTransportServices | ||
import NIOSSL | ||
|
||
/// `EventLoopGroupManager` can be used to manage an `EventLoopGroup`, either by creating or by sharing an existing one. | ||
/// | ||
/// When making network client libraries with SwiftNIO that are supposed to work well on both Apple platforms (macOS, | ||
/// iOS, tvOS, ...) as well as Linux, users often struggle with the task of selecting the right combination of: | ||
/// | ||
/// - an `EventLoopGroup` | ||
/// - a bootstrap | ||
/// - a TLS implementation | ||
/// | ||
/// The choices to the above need to be compatible, or else the program won't work. | ||
/// | ||
/// What makes the task even harder is that as a client library, you often want to share the `EventLoopGroup` with other | ||
/// components. That raises the question of how to choose a bootstrap and a matching TLS implementation without even | ||
/// knowing the concrete `EventLoopGroup` type (it may be `SelectableEventLoop` which is an internal `NIO` types). | ||
/// `EventLoopGroupManager` should support all those use cases with a simple API. | ||
public class EventLoopGroupManager { | ||
private var group: Optional<EventLoopGroup> | ||
private let provider: Provider | ||
private var sslContext = try! NIOSSLContext(configuration: .forClient()) | ||
|
||
public enum Provider { | ||
case createNew | ||
case shared(EventLoopGroup) | ||
} | ||
|
||
/// Initialize the `EventLoopGroupManager` with a `Provder` of `EventLoopGroup`s. | ||
/// | ||
/// The `Provider` lets you choose whether to `.share(aGroup)` or to `.createNew`. | ||
public init(provider: Provider) { | ||
self.provider = provider | ||
switch self.provider { | ||
case .shared(let group): | ||
self.group = group | ||
case .createNew: | ||
self.group = nil | ||
} | ||
} | ||
|
||
deinit { | ||
assert(self.group == nil, "Please call EventLoopGroupManager.syncShutdown .") | ||
} | ||
} | ||
|
||
// - MARK: Public API | ||
extension EventLoopGroupManager { | ||
/// Create a "universal bootstrap" for the given host. | ||
/// | ||
/// - parameters: | ||
/// - hostname: The hostname to connect to (for SNI). | ||
/// - useTLS: Whether to use TLS or not. | ||
public func makeBootstrap(hostname: String, useTLS: Bool = true) throws -> NIOClientTCPBootstrap { | ||
let bootstrap: NIOClientTCPBootstrap | ||
|
||
if let group = self.group { | ||
bootstrap = try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: hostname) | ||
} else { | ||
bootstrap = try self.makeUniversalBootstrapWithSystemDefaults(serverHostname: hostname) | ||
} | ||
|
||
if useTLS { | ||
return bootstrap.enableTLS() | ||
} else { | ||
return bootstrap | ||
} | ||
} | ||
|
||
/// Shutdown the `EventLoopGroupManager`. | ||
/// | ||
/// This will release all resources associated with the `EventLoopGroupManager` such as the threads that the | ||
/// `EventLoopGroup` runs on. | ||
/// | ||
/// This method _must_ be called when you're done with this `EventLoopGroupManager`. | ||
public func syncShutdown() throws { | ||
switch self.provider { | ||
case .createNew: | ||
try self.group?.syncShutdownGracefully() | ||
case .shared: | ||
() // nothing to do. | ||
} | ||
self.group = nil | ||
} | ||
} | ||
|
||
// - MARK: Error types | ||
extension EventLoopGroupManager { | ||
/// The provided `EventLoopGroup` is not compatible with this client. | ||
public struct UnsupportedEventLoopGroupError: Error { | ||
var eventLoopGroup: EventLoopGroup | ||
} | ||
} | ||
|
||
// - MARK: Internal functions | ||
extension EventLoopGroupManager { | ||
// This function combines the right pieces and returns you a "universal client bootstrap" | ||
// (`NIOClientTCPBootstrap`). This allows you to bootstrap connections (with or without TLS) using either the | ||
// NIO on sockets (`NIO`) or NIO on Network.framework (`NIOTransportServices`) stacks. | ||
// The remainder of the code should be platform-independent. | ||
private func makeUniversalBootstrapWithSystemDefaults(serverHostname: String) throws -> NIOClientTCPBootstrap { | ||
if let group = self.group { | ||
return try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: serverHostname) | ||
} | ||
|
||
let group: EventLoopGroup | ||
#if canImport(Network) | ||
if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 3, *) { | ||
// We run on a new-enough Darwin so we can use Network.framework | ||
group = NIOTSEventLoopGroup() | ||
} else { | ||
// We're on Darwin but not new enough for Network.framework, so we fall back on NIO on BSD sockets. | ||
group = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
} | ||
#else | ||
// We are on a non-Darwin platform, so we'll use BSD sockets. | ||
group = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
#endif | ||
|
||
// Let's save it for next time. | ||
self.group = group | ||
return try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: serverHostname) | ||
} | ||
|
||
// If we already know the group, then let's just contruct the correct bootstrap. | ||
private func makeUniversalBootstrapWithExistingGroup(_ group: EventLoopGroup, | ||
serverHostname: String) throws -> NIOClientTCPBootstrap { | ||
if let bootstrap = ClientBootstrap(validatingGroup: group) { | ||
return try NIOClientTCPBootstrap(bootstrap, | ||
tls: NIOSSLClientTLSProvider(context: self.sslContext, | ||
serverHostname: serverHostname)) | ||
} | ||
|
||
#if canImport(Network) | ||
if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 3, *) { | ||
if let makeBootstrap = NIOTSConnectionBootstrap(validatingGroup: group) { | ||
return NIOClientTCPBootstrap(makeBootstrap, tls: NIOTSClientTLSProvider()) | ||
} | ||
} | ||
#endif | ||
|
||
throw UnsupportedEventLoopGroupError(eventLoopGroup: group) | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
UniversalBootstrapDemo/Sources/UniversalBootstrapDemo/ExampleHTTPLibrary.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import NIO | ||
import NIOHTTP1 | ||
import Foundation | ||
|
||
public struct UnsupportedURLError: Error { | ||
var url: String | ||
} | ||
|
||
public class ExampleHTTPLibrary { | ||
let groupManager: EventLoopGroupManager | ||
|
||
public init(groupProvider provider: EventLoopGroupManager.Provider) { | ||
self.groupManager = EventLoopGroupManager(provider: provider) | ||
} | ||
|
||
public func shutdown() throws { | ||
try self.groupManager.syncShutdown() | ||
} | ||
|
||
public func makeRequest(url urlString: String) throws { | ||
final class PrintToStdoutHandler: ChannelInboundHandler { | ||
|
||
typealias InboundIn = HTTPClientResponsePart | ||
|
||
func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
switch self.unwrapInboundIn(data) { | ||
case .head: | ||
() // ignore | ||
case .body(let buffer): | ||
buffer.withUnsafeReadableBytes { ptr in | ||
_ = write(STDOUT_FILENO, ptr.baseAddress, ptr.count) | ||
} | ||
case .end: | ||
context.close(promise: nil) | ||
} | ||
} | ||
} | ||
|
||
guard let url = URL(string: urlString), | ||
let hostname = url.host, | ||
let scheme = url.scheme?.lowercased(), | ||
["http", "https"].contains(scheme.lowercased()) else { | ||
throw UnsupportedURLError(url: urlString) | ||
} | ||
let uri = url.path | ||
let useTLS = url.scheme?.lowercased() == "https" ? true : false | ||
let connection = try groupManager.makeBootstrap(hostname: hostname, useTLS: useTLS) | ||
.channelInitializer { channel in | ||
channel.pipeline.addHTTPClientHandlers().flatMap { | ||
channel.pipeline.addHandler(PrintToStdoutHandler()) | ||
} | ||
} | ||
.connect(host: hostname, port: useTLS ? 443 : 80) | ||
.wait() | ||
print("# Channel") | ||
print(connection) | ||
print("# ChannelPipeline") | ||
print("\(connection.pipeline)") | ||
print("# HTTP response body") | ||
let reqHead = HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1), | ||
method: .GET, | ||
uri: uri, | ||
headers: ["host": hostname])) | ||
connection.write(reqHead, promise: nil) | ||
try connection.writeAndFlush(HTTPClientRequestPart.end(nil)).wait() | ||
try connection.closeFuture.wait() | ||
} | ||
} |
Oops, something went wrong.