From e82a6c5b12be8225a1bf9e60d0abd9b27731d831 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 7 Jul 2023 11:48:46 +0200 Subject: [PATCH] Add async TCP echo example # Motivation We introduced new async APIs to our bootstraps that make it easy to use NIO from Swift Concurrency. We should provide examples showing how to use those new APIs. In the future, we want to make those examples the primary ones and remove the non-async based ones. # Modification This PR introduces two new targets: `NIOTCPEchoServer` and `NIOTCPEchoClient`. Both targets are executable and provide either the server or client piece of the example. # Result New and shiny async example --- Package.swift | 16 +++++ Sources/NIOTCPEchoClient/Client.swift | 91 +++++++++++++++++++++++++ Sources/NIOTCPEchoClient/README.md | 11 +++ Sources/NIOTCPEchoServer/README.md | 11 +++ Sources/NIOTCPEchoServer/Server.swift | 96 +++++++++++++++++++++++++++ 5 files changed, 225 insertions(+) create mode 100644 Sources/NIOTCPEchoClient/Client.swift create mode 100644 Sources/NIOTCPEchoClient/README.md create mode 100644 Sources/NIOTCPEchoServer/README.md create mode 100644 Sources/NIOTCPEchoServer/Server.swift diff --git a/Package.swift b/Package.swift index 467f79d147..0513cc65cd 100644 --- a/Package.swift +++ b/Package.swift @@ -171,6 +171,22 @@ let package = Package( // MARK: - Examples + .executableTarget( + name: "NIOTCPEchoServer", + dependencies: [ + "NIOPosix", + "NIOCore", + ], + exclude: ["README.md"] + ), + .executableTarget( + name: "NIOTCPEchoClient", + dependencies: [ + "NIOPosix", + "NIOCore", + ], + exclude: ["README.md"] + ), .executableTarget( name: "NIOEchoServer", dependencies: [ diff --git a/Sources/NIOTCPEchoClient/Client.swift b/Sources/NIOTCPEchoClient/Client.swift new file mode 100644 index 0000000000..49d7b77573 --- /dev/null +++ b/Sources/NIOTCPEchoClient/Client.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// +#if swift(>=5.9) +@_spi(AsyncChannel) import NIOCore +@_spi(AsyncChannel) import NIOPosix + +@available(macOS 14, *) +@main +struct Client { + /// The host to connect to. + private let host: String + /// The port to connect to. + private let port: Int + /// The client's event loop group. + private let eventLoopGroup: any EventLoopGroup + + static func main() async throws { + // We are creating this event loop group at the top level and it will live until + // the process exits. This means we also don't have to shut it down. + // + // Note that we start this group with 1 thread. In general, most NIO programs + // should use 1 thread as a default unless they're planning to be a network + // proxy or something that is expecting to be dominated by packet parsing. Most + // servers aren't, and NIO is very fast, so 1 NIO thread is quite capable of + // saturating the average small to medium sized machine. + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + let server = Client( + host: "localhost", + port: 8765, + eventLoopGroup: eventLoopGroup + ) + try await server.run() + + print("Done sending requests; exiting in 5 seconds") + try await Task.sleep(for: .seconds(5)) + } + + /// This method starts the server and handles incoming connections. + func run() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for i in 0...20 { + group.addTask { + try await self.sendRequest(number: i) + } + } + + try await group.waitForAll() + } + } + + private func sendRequest(number: Int) async throws { + let channel = try await ClientBootstrap(group: eventLoopGroup) + .connect( + host: self.host, + port: self.port, + channelConfiguration: .init( + inboundType: ByteBuffer.self, + outboundType: ByteBuffer.self + ) + ) + + print("Connection(\(number)): Writing request") + try await channel.outboundWriter.write(ByteBuffer(string: "Hello on connection \(number)")) + + for try await inboundData in channel.inboundStream { + print("Connection(\(number)): Received response (\(String(buffer: inboundData)))") + + // We only expect a single response so we can exit here + break + } + } +}#else +@main +struct Client { + static func main() { + fatalError("Requires at least Swift 5.9") + } +} +#endif diff --git a/Sources/NIOTCPEchoClient/README.md b/Sources/NIOTCPEchoClient/README.md new file mode 100644 index 0000000000..305d452a49 --- /dev/null +++ b/Sources/NIOTCPEchoClient/README.md @@ -0,0 +1,11 @@ +# NIOTCPEchoClient + +This sample application provides a simple TCP echo client that will send multiple messages to an +echo server and wait for a response of all of them. Before running this client, make sure to start +the `NIOTCPEchoServer`. + +To run this client execute the following from the root of the repository: + +```bash +swift run NIOTCPEchoClient +``` diff --git a/Sources/NIOTCPEchoServer/README.md b/Sources/NIOTCPEchoServer/README.md new file mode 100644 index 0000000000..ba6f80323a --- /dev/null +++ b/Sources/NIOTCPEchoServer/README.md @@ -0,0 +1,11 @@ +# NIOTCPEchoServer + +This sample application provides a simple TCP server that sends clients back whatever they send it. + +To run this server execute the following from the root of the repository: + +```bash +swift run NIOTCPEchoServer +``` + +You can then use the `NIOTCPClient` to send requests to the server. diff --git a/Sources/NIOTCPEchoServer/Server.swift b/Sources/NIOTCPEchoServer/Server.swift new file mode 100644 index 0000000000..f5558de456 --- /dev/null +++ b/Sources/NIOTCPEchoServer/Server.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// +#if swift(>=5.9) +@_spi(AsyncChannel) import NIOCore +@_spi(AsyncChannel) import NIOPosix + +@available(macOS 14, *) +@main +struct Server { + /// The server's host. + private let host: String + /// The server's port. + private let port: Int + /// The server's event loop group. + private let eventLoopGroup: any EventLoopGroup + + static func main() async throws { + // We are creating this event loop group at the top level and it will live until + // the process exits. This means we also don't have to shut it down. + // + // Note that we start this group with 1 thread. In general, most NIO programs + // should use 1 thread as a default unless they're planning to be a network + // proxy or something that is expecting to be dominated by packet parsing. Most + // servers aren't, and NIO is very fast, so 1 NIO thread is quite capable of + // saturating the average small to medium sized machine. + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + let server = Server( + host: "localhost", + port: 8765, + eventLoopGroup: eventLoopGroup + ) + try await server.run() + } + + /// This method starts the server and handles incoming connections. + func run() async throws { + let channel = try await ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .bind( + host: self.host, + port: self.port, + childChannelConfiguration: .init( + inboundType: ByteBuffer.self, + outboundType: ByteBuffer.self + ) + ) + + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks + // otherwise this task group would end up leaking memory of all finished connection tasks. + try await withThrowingDiscardingTaskGroup { group in + for try await connectionChannel in channel.inboundStream { + group.addTask { + print("Handling new connection") + await self.handleConnection(channel: connectionChannel) + print("Done handling connection") + } + } + } + } + + /// This method handles a single connection by echoing back all inbound data. + private func handleConnection(channel: NIOAsyncChannel) async { + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + for try await inboundData in channel.inboundStream { + print("Received request (\(String(buffer: inboundData)))") + try await channel.outboundWriter.write(inboundData) + } + } catch { + print("Hit error: \(error)") + } + } +} +#else +@main +struct Server { + static func main() { + fatalError("Requires at least Swift 5.9") + } +} +#endif