Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

full example demonstrating the universal bootstrap #48

Merged
merged 1 commit into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions UniversalBootstrapDemo/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// swift-tools-version:5.1
glbrntt marked this conversation as resolved.
Show resolved Hide resolved
// 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"]),
]
)
93 changes: 93 additions & 0 deletions UniversalBootstrapDemo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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).

## Understanding this example

This example mainly consists of three files:

- [`EventLoopGroupManager.swift`](Sources/UniversalBootstrapDemo/EventLoopGroupManager.swift) which is the main and most important component of this example. It demonstrates a way how you can manage your `EventLoopGroup`, select a matching bootstrap, as well as a TLS implementation.
- [`ExampleHTTPLibrary.swift`](Sources/UniversalBootstrapDemo/ExampleHTTPLibrary.swift) which is an example of how you could implement a basic HTTP library using `EventLoopGroupManager`.
- [`main.swift`](Sources/UniversalBootstrapDemo/main.swift) which is just the driver to run the example programs.

## 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
[I] ↓↑ [O]
↓↑ HTTPRequestEncoder [handler0]
HTTPResponseDecoder ↓↑ HTTPResponseDecoder [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
[I] ↓↑ [O]
NIOSSLClientHandler ↓↑ NIOSSLClientHandler [handler3]
↓↑ HTTPRequestEncoder [handler0]
HTTPResponseDecoder ↓↑ HTTPResponseDecoder [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"
}
```
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 find it tedious to select 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 use a `.shared(group)` 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 6, *) {
// 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 6, *) {
if let makeBootstrap = NIOTSConnectionBootstrap(validatingGroup: group) {
return NIOClientTCPBootstrap(makeBootstrap, tls: NIOTSClientTLSProvider())
}
}
#endif

throw UnsupportedEventLoopGroupError(eventLoopGroup: group)
}
}
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()
}
}
Loading