diff --git a/Sources/NIOTransportServices/NIOTSBootstraps.swift b/Sources/NIOTransportServices/NIOTSBootstraps.swift new file mode 100644 index 0000000..798b561 --- /dev/null +++ b/Sources/NIOTransportServices/NIOTSBootstraps.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Shared functionality across NIOTS bootstraps. +internal enum NIOTSBootstraps { + @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) + internal static func isCompatible(group: EventLoopGroup) -> Bool { + return group is NIOTSEventLoop || group is NIOTSEventLoopGroup + } +} diff --git a/Sources/NIOTransportServices/NIOTSConnectionBootstrap.swift b/Sources/NIOTransportServices/NIOTSConnectionBootstrap.swift index 1848bc3..2224a96 100644 --- a/Sources/NIOTransportServices/NIOTSConnectionBootstrap.swift +++ b/Sources/NIOTransportServices/NIOTSConnectionBootstrap.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,6 +17,27 @@ import NIO import Dispatch import Network +/// A `NIOTSConnectionBootstrap` is an easy way to bootstrap a `NIOTSConnectionChannel` when creating network clients. +/// +/// Usually you re-use a `NIOTSConnectionBootstrap` once you set it up and called `connect` multiple times on it. +/// This way you ensure that the same `EventLoop`s will be shared across all your connections. +/// +/// Example: +/// +/// ```swift +/// let group = NIOTSEventLoopGroup() +/// defer { +/// try! group.syncShutdownGracefully() +/// } +/// let bootstrap = NIOTSConnectionBootstrap(group: group) +/// .channelInitializer { channel in +/// channel.pipeline.addHandler(MyChannelHandler()) +/// } +/// try! bootstrap.connect(host: "example.org", port: 12345).wait() +/// /* the Channel is now connected */ +/// ``` +/// +/// The connected `NIOTSConnectionChannel` will operate on `ByteBuffer` as inbound and on `IOData` as outbound messages. @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) public final class NIOTSConnectionBootstrap { private let group: EventLoopGroup @@ -30,20 +51,20 @@ public final class NIOTSConnectionBootstrap { /// Create a `NIOTSConnectionBootstrap` on the `EventLoopGroup` `group`. /// - /// This initializer only exists to be more in-line with the NIO core bootstraps, in that they - /// may be constructed with an `EventLoopGroup` and by extenstion an `EventLoop`. As such an - /// existing `NIOTSEventLoop` may be used to initialize this bootstrap. Where possible the - /// initializers accepting `NIOTSEventLoopGroup` should be used instead to avoid the wrong - /// type being used. - /// - /// Note that the "real" solution is described in https://github.com/apple/swift-nio/issues/674. + /// The `EventLoopGroup` `group` must be compatible, otherwise the program will crash. `NIOTSConnectionBootstrap` is + /// compatible only with `NIOTSEventLoopGroup` as well as the `EventLoop`s returned by + /// `NIOTSEventLoopGroup.next`. See `init(validatingGroup:)` for a fallible initializer for + /// situations where it's impossible to tell ahead of time if the `EventLoopGroup` is compatible or not. /// /// - parameters: /// - group: The `EventLoopGroup` to use. - public init(group: EventLoopGroup) { - self.group = group + public convenience init(group: EventLoopGroup) { + guard NIOTSBootstraps.isCompatible(group: group) else { + preconditionFailure("NIOTSConnectionBootstrap is only compatible with NIOTSEventLoopGroup and " + + "NIOTSEventLoop. You tried constructing one with \(group) which is incompatible.") + } - self.channelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) + self.init(validatingGroup: group)! } /// Create a `NIOTSConnectionBootstrap` on the `NIOTSEventLoopGroup` `group`. @@ -54,6 +75,20 @@ public final class NIOTSConnectionBootstrap { self.init(group: group as EventLoopGroup) } + /// Create a `NIOTSConnectionBootstrap` on the `NIOTSEventLoopGroup` `group`, validating + /// that the `EventLoopGroup` is compatible with `NIOTSConnectionBootstrap`. + /// + /// - parameters: + /// - group: The `EventLoopGroup` to use. + public init?(validatingGroup group: EventLoopGroup) { + guard NIOTSBootstraps.isCompatible(group: group) else { + return nil + } + + self.group = group + self.channelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) + } + /// Initialize the connected `NIOTSConnectionChannel` with `initializer`. The most common task in initializer is to add /// `ChannelHandler`s to the `ChannelPipeline`. /// diff --git a/Sources/NIOTransportServices/NIOTSEventLoop.swift b/Sources/NIOTransportServices/NIOTSEventLoop.swift index b452b35..aad4af0 100644 --- a/Sources/NIOTransportServices/NIOTSEventLoop.swift +++ b/Sources/NIOTransportServices/NIOTSEventLoop.swift @@ -30,7 +30,7 @@ public protocol QoSEventLoop: EventLoop { /// Submit a given task to be executed by the `EventLoop` at a given `qos`. func execute(qos: DispatchQoS, _ task: @escaping () -> Void) -> Void - /// Schedule a `task` that is executed by this `SelectableEventLoop` after the given amount of time at the + /// Schedule a `task` that is executed by this `NIOTSEventLoop` after the given amount of time at the /// given `qos`. func scheduleTask(in time: TimeAmount, qos: DispatchQoS, _ task: @escaping () throws -> T) -> Scheduled } diff --git a/Sources/NIOTransportServices/NIOTSListenerBootstrap.swift b/Sources/NIOTransportServices/NIOTSListenerBootstrap.swift index fbf775d..1ebfcf1 100644 --- a/Sources/NIOTransportServices/NIOTSListenerBootstrap.swift +++ b/Sources/NIOTransportServices/NIOTSListenerBootstrap.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,6 +17,42 @@ import NIO import Dispatch import Network +/// A `NIOTSListenerBootstrap` is an easy way to bootstrap a `NIOTSListenerChannel` when creating network servers. +/// +/// Example: +/// +/// ```swift +/// let group = NIOTSEventLoopGroup() +/// defer { +/// try! group.syncShutdownGracefully() +/// } +/// let bootstrap = NIOTSListenerBootstrap(group: group) +/// // Specify backlog and enable SO_REUSEADDR for the server itself +/// .serverChannelOption(ChannelOptions.backlog, value: 256) +/// .serverChannelOption(ChannelOptions.socketOption(.reuseaddr), value: 1) +/// +/// // Set the handlers that are applied to the accepted child `Channel`s. +/// .childChannelInitializer { channel in +/// // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. +/// channel.pipeline.addHandler(BackPressureHandler()).flatMap { () in +/// // make sure to instantiate your `ChannelHandlers` inside of +/// // the closure as it will be invoked once per connection. +/// channel.pipeline.addHandler(MyChannelHandler()) +/// } +/// } +/// let channel = try! bootstrap.bind(host: host, port: port).wait() +/// /* the server will now be accepting connections */ +/// +/// try! channel.closeFuture.wait() // wait forever as we never close the Channel +/// ``` +/// +/// The `EventLoopFuture` returned by `bind` will fire with a `NIOTSListenerChannel`. This is the channel that owns the +/// listening socket. Each time it accepts a new connection it will fire a `NIOTSConnectionChannel` through the +/// `ChannelPipeline` via `fireChannelRead`: as a result, the `NIOTSListenerChannel` operates on `Channel`s as inbound +/// messages. Outbound messages are not supported on a `NIOTSListenerChannel` which means that each write attempt will +/// fail. +/// +/// Accepted `NIOTSConnectionChannel`s operate on `ByteBuffer` as inbound data, and `IOData` as outbound data. @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) public final class NIOTSListenerBootstrap { private let group: EventLoopGroup @@ -42,18 +78,15 @@ public final class NIOTSListenerBootstrap { /// Note that the "real" solution is described in https://github.com/apple/swift-nio/issues/674. /// /// - parameters: - /// - group: The `EventLoopGroup` to use for the `ServerSocketChannel`. + /// - group: The `EventLoopGroup` to use for the `NIOTSListenerChannel`. public convenience init(group: EventLoopGroup) { self.init(group: group, childGroup: group) - - self.serverChannelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) - self.childChannelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) } /// Create a `NIOTSListenerBootstrap` for the `NIOTSEventLoopGroup` `group`. /// /// - parameters: - /// - group: The `NIOTSEventLoopGroup` to use for the `ServerSocketChannel`. + /// - group: The `NIOTSEventLoopGroup` to use for the `NIOTSListenerChannel`. public convenience init(group: NIOTSEventLoopGroup) { self.init(group: group as EventLoopGroup) } @@ -72,7 +105,29 @@ public final class NIOTSListenerBootstrap { /// - group: The `EventLoopGroup` to use for the `bind` of the `NIOTSListenerChannel` /// and to accept new `NIOTSConnectionChannel`s with. /// - childGroup: The `EventLoopGroup` to run the accepted `NIOTSConnectionChannel`s on. - public init(group: EventLoopGroup, childGroup: EventLoopGroup) { + public convenience init(group: EventLoopGroup, childGroup: EventLoopGroup) { + guard NIOTSBootstraps.isCompatible(group: group) && NIOTSBootstraps.isCompatible(group: childGroup) else { + preconditionFailure("NIOTSListenerBootstrap is only compatible with NIOTSEventLoopGroup and " + + "NIOTSEventLoop. You tried constructing one with group: \(group) and " + + "childGroup: \(childGroup) at least one of which is incompatible.") + } + + self.init(validatingGroup: group, childGroup: childGroup)! + } + + /// Create a `NIOTSListenerBootstrap` on the `EventLoopGroup` `group` which accepts `Channel`s on `childGroup`, + /// validating that the `EventLoopGroup`s are compatible with `NIOTSListenerBootstrap`. + /// + /// - parameters: + /// - group: The `EventLoopGroup` to use for the `bind` of the `NIOTSListenerChannel` + /// and to accept new `NIOTSConnectionChannel`s with. + /// - childGroup: The `EventLoopGroup` to run the accepted `NIOTSConnectionChannel`s on. + public init?(validatingGroup group: EventLoopGroup, childGroup: EventLoopGroup? = nil) { + let childGroup = childGroup ?? group + guard NIOTSBootstraps.isCompatible(group: group) && NIOTSBootstraps.isCompatible(group: childGroup) else { + return nil + } + self.group = group self.childGroup = childGroup @@ -96,7 +151,7 @@ public final class NIOTSListenerBootstrap { /// The `NIOTSListenerChannel` uses the accepted `NIOTSConnectionChannel`s as inbound messages. /// /// - note: To set the initializer for the accepted `NIOTSConnectionChannel`s, look at - /// `ServerBootstrap.childChannelInitializer`. + /// `NIOTSConnectionBootstrap.childChannelInitializer`. /// /// - parameters: /// - initializer: A closure that initializes the provided `Channel`. diff --git a/Tests/NIOTransportServicesTests/NIOTSBootstrapTests.swift b/Tests/NIOTransportServicesTests/NIOTSBootstrapTests.swift index e8b9ddd..11ed8de 100644 --- a/Tests/NIOTransportServicesTests/NIOTSBootstrapTests.swift +++ b/Tests/NIOTransportServicesTests/NIOTSBootstrapTests.swift @@ -183,6 +183,69 @@ final class NIOTSBootstrapTests: XCTestCase { XCTAssertNoThrow(XCTAssertFalse(try isTLSConnection1.futureResult.wait())) XCTAssertNoThrow(XCTAssertTrue(try isTLSConnection2.futureResult.wait())) } + + func testNIOTSConnectionBootstrapValidatesWorkingELGsCorrectly() { + let elg = NIOTSEventLoopGroup() + defer { + XCTAssertNoThrow(try elg.syncShutdownGracefully()) + } + let el = elg.next() + + XCTAssertNotNil(NIOTSConnectionBootstrap(validatingGroup: elg)) + XCTAssertNotNil(NIOTSConnectionBootstrap(validatingGroup: el)) + } + + func testNIOTSConnectionBootstrapRejectsNotWorkingELGsCorrectly() { + let elg = EmbeddedEventLoop() + defer { + XCTAssertNoThrow(try elg.syncShutdownGracefully()) + } + let el = elg.next() + + XCTAssertNil(NIOTSConnectionBootstrap(validatingGroup: elg)) + XCTAssertNil(NIOTSConnectionBootstrap(validatingGroup: el)) + } + + func testNIOTSListenerBootstrapValidatesWorkingELGsCorrectly() { + let elg = NIOTSEventLoopGroup() + defer { + XCTAssertNoThrow(try elg.syncShutdownGracefully()) + } + let el = elg.next() + + XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: elg)) + XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: el)) + XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: elg, childGroup: elg)) + XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: el, childGroup: el)) + } + + func testNIOTSListenerBootstrapRejectsNotWorkingELGsCorrectly() { + let correctELG = NIOTSEventLoopGroup() + defer { + XCTAssertNoThrow(try correctELG.syncShutdownGracefully()) + } + + let wrongELG = EmbeddedEventLoop() + defer { + XCTAssertNoThrow(try wrongELG.syncShutdownGracefully()) + } + let wrongEL = wrongELG.next() + let correctEL = correctELG.next() + + // both wrong + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG)) + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL)) + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG, childGroup: wrongELG)) + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL, childGroup: wrongEL)) + + // group correct, child group wrong + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: correctELG, childGroup: wrongELG)) + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: correctEL, childGroup: wrongEL)) + + // group wrong, child group correct + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG, childGroup: correctELG)) + XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL, childGroup: correctEL)) + } } extension Channel {