-
Notifications
You must be signed in to change notification settings - Fork 656
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# 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
- Loading branch information
1 parent
5f54289
commit e82a6c5
Showing
5 changed files
with
225 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
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,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 |
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,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 | ||
``` |
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,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. |
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,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<ByteBuffer, ByteBuffer>) 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 |