Skip to content

Commit

Permalink
full example demonstrating the universal bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
weissi committed May 5, 2020
1 parent 4bdfd14 commit 630205e
Show file tree
Hide file tree
Showing 5 changed files with 417 additions and 0 deletions.
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
// 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"]),
]
)
87 changes: 87 additions & 0 deletions UniversalBootstrapDemo/README.md
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"
}
```
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)
}
}
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

0 comments on commit 630205e

Please sign in to comment.