Skip to content

Commit

Permalink
Add async TCP echo example
Browse files Browse the repository at this point in the history
# 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
FranzBusch committed Jul 11, 2023
1 parent 5f54289 commit e82a6c5
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
91 changes: 91 additions & 0 deletions Sources/NIOTCPEchoClient/Client.swift
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
11 changes: 11 additions & 0 deletions Sources/NIOTCPEchoClient/README.md
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
```
11 changes: 11 additions & 0 deletions Sources/NIOTCPEchoServer/README.md
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.
96 changes: 96 additions & 0 deletions Sources/NIOTCPEchoServer/Server.swift
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

0 comments on commit e82a6c5

Please sign in to comment.