Bläddra i källkod

Add Echo server and client example using async-await API (#1278)

Si Beaumont 4 år sedan
förälder
incheckning
ab56e28e8b

+ 18 - 0
Package.swift

@@ -284,6 +284,23 @@ extension Target {
     path: "Sources/Examples/Echo/Runtime"
   )
 
+  static let echoAsync: Target = .target(
+    name: "AsyncAwaitEcho",
+    dependencies: [
+      .grpc,
+      .echoModel,
+      .echoImplementation,
+      .grpcSampleData,
+      .nioCore,
+      .nioPosix,
+      .logging,
+      .argumentParser,
+    ].appending(
+      .nioSSL, if: includeNIOSSL
+    ),
+    path: "Sources/Examples/Echo/AsyncAwaitRuntime"
+  )
+
   static let helloWorldModel: Target = .target(
     name: "HelloWorldModel",
     dependencies: [
@@ -416,6 +433,7 @@ let package = Package(
     .echoModel,
     .echoImplementation,
     .echo,
+    .echoAsync,
     .helloWorldModel,
     .helloWorldClient,
     .helloWorldServer,

+ 52 - 0
Sources/Examples/Echo/AsyncAwaitRuntime/ArgumentParser+AsyncAwait.swift

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// NOTE: This file should be removed when the `async` branch of `swift-argument-parser` has been
+///       released: https://github.com/apple/swift-argument-parser/tree/async
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+
+import ArgumentParser
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+protocol AsyncParsableCommand: ParsableCommand {
+  mutating func runAsync() async throws
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+extension AsyncParsableCommand {
+  public mutating func run() throws {
+    throw CleanExit.helpRequest(self)
+  }
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+extension ParsableCommand {
+  static func main(_ arguments: [String]? = nil) async {
+    do {
+      var command = try parseAsRoot(arguments)
+      if var asyncCommand = command as? AsyncParsableCommand {
+        try await asyncCommand.runAsync()
+      } else {
+        try command.run()
+      }
+    } catch {
+      exit(withError: error)
+    }
+  }
+}
+
+#endif

+ 273 - 0
Sources/Examples/Echo/AsyncAwaitRuntime/main.swift

@@ -0,0 +1,273 @@
+/*
+ * Copyright 2021, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+
+import ArgumentParser
+import EchoImplementation
+import EchoModel
+import GRPC
+import GRPCSampleData
+import NIOCore
+import NIOPosix
+#if canImport(NIOSSL)
+import NIOSSL
+#endif
+
+// MARK: - Argument parsing
+
+enum RPC: String, ExpressibleByArgument {
+  case get
+  case collect
+  case expand
+  case update
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+struct Echo: ParsableCommand {
+  static var configuration = CommandConfiguration(
+    abstract: "An example to run and call a simple gRPC service for echoing messages.",
+    subcommands: [Server.self, Client.self]
+  )
+
+  struct Server: AsyncParsableCommand {
+    static var configuration = CommandConfiguration(
+      abstract: "Start a gRPC server providing the Echo service."
+    )
+
+    @Option(help: "The port to listen on for new connections")
+    var port = 1234
+
+    @Flag(help: "Whether TLS should be used or not")
+    var tls = false
+
+    func runAsync() async throws {
+      let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+      defer {
+        try! group.syncShutdownGracefully()
+      }
+      do {
+        try await startEchoServer(group: group, port: self.port, useTLS: self.tls)
+      } catch {
+        print("Error running server: \(error)")
+      }
+    }
+  }
+
+  struct Client: AsyncParsableCommand {
+    static var configuration = CommandConfiguration(
+      abstract: "Calls an RPC on the Echo server."
+    )
+
+    @Option(help: "The port to connect to")
+    var port = 1234
+
+    @Flag(help: "Whether TLS should be used or not")
+    var tls = false
+
+    @Flag(help: "Whether interceptors should be used, see 'docs/interceptors-tutorial.md'.")
+    var intercept = false
+
+    @Option(help: "RPC to call ('get', 'collect', 'expand', 'update').")
+    var rpc: RPC = .get
+
+    @Option(help: "How many RPCs to do.")
+    var iterations: Int = 1
+
+    @Argument(help: "Message to echo")
+    var message: String
+
+    func runAsync() async throws {
+      let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+      defer {
+        try! group.syncShutdownGracefully()
+      }
+
+      let client = makeClient(
+        group: group,
+        port: self.port,
+        useTLS: self.tls,
+        useInterceptor: self.intercept
+      )
+      defer {
+        try! client.channel.close().wait()
+      }
+
+      for _ in 0 ..< self.iterations {
+        await callRPC(self.rpc, using: client, message: self.message)
+      }
+    }
+  }
+}
+
+// MARK: - Server
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func startEchoServer(group: EventLoopGroup, port: Int, useTLS: Bool) async throws {
+  let builder: Server.Builder
+
+  if useTLS {
+    #if canImport(NIOSSL)
+    // We're using some self-signed certs here: check they aren't expired.
+    let caCert = SampleCertificate.ca
+    let serverCert = SampleCertificate.server
+    precondition(
+      !caCert.isExpired && !serverCert.isExpired,
+      "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift."
+    )
+
+    builder = Server.usingTLSBackedByNIOSSL(
+      on: group,
+      certificateChain: [serverCert.certificate],
+      privateKey: SamplePrivateKey.server
+    )
+    .withTLS(trustRoots: .certificates([caCert.certificate]))
+    print("starting secure server")
+    #else
+    fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available")
+    #endif // canImport(NIOSSL)
+  } else {
+    print("starting insecure server")
+    builder = Server.insecure(group: group)
+  }
+
+  let server = try await builder.withServiceProviders([EchoAsyncProvider()])
+    .bind(host: "localhost", port: port)
+    .get()
+
+  print("started server: \(server.channel.localAddress!)")
+
+  // This blocks to keep the main thread from finishing while the server runs,
+  // but the server never exits. Kill the process to stop it.
+  try await server.onClose.get()
+}
+
+// MARK: - Client
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func makeClient(
+  group: EventLoopGroup,
+  port: Int,
+  useTLS: Bool,
+  useInterceptor: Bool
+) -> Echo_EchoAsyncClient {
+  let builder: ClientConnection.Builder
+
+  if useTLS {
+    #if canImport(NIOSSL)
+    // We're using some self-signed certs here: check they aren't expired.
+    let caCert = SampleCertificate.ca
+    let clientCert = SampleCertificate.client
+    precondition(
+      !caCert.isExpired && !clientCert.isExpired,
+      "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift."
+    )
+
+    builder = ClientConnection.usingTLSBackedByNIOSSL(on: group)
+      .withTLS(certificateChain: [clientCert.certificate])
+      .withTLS(privateKey: SamplePrivateKey.client)
+      .withTLS(trustRoots: .certificates([caCert.certificate]))
+    #else
+    fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available")
+    #endif // canImport(NIOSSL)
+  } else {
+    builder = ClientConnection.insecure(group: group)
+  }
+
+  // Start the connection and create the client:
+  let connection = builder.connect(host: "localhost", port: port)
+
+  return Echo_EchoAsyncClient(
+    channel: connection,
+    interceptors: useInterceptor ? ExampleClientInterceptorFactory() : nil
+  )
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func callRPC(_ rpc: RPC, using client: Echo_EchoAsyncClient, message: String) async {
+  do {
+    switch rpc {
+    case .get:
+      try await echoGet(client: client, message: message)
+    case .collect:
+      try await echoCollect(client: client, message: message)
+    case .expand:
+      try await echoExpand(client: client, message: message)
+    case .update:
+      try await echoUpdate(client: client, message: message)
+    }
+  } catch {
+    print("\(rpc) RPC failed: \(error)")
+  }
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func echoGet(client: Echo_EchoAsyncClient, message: String) async throws {
+  let response = try await client.get(.with { $0.text = message })
+  print("get received: \(response.text)")
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func echoCollect(client: Echo_EchoAsyncClient, message: String) async throws {
+  let messages = message.components(separatedBy: " ").map { part in
+    Echo_EchoRequest.with { $0.text = part }
+  }
+  let response = try await client.collect(messages)
+  print("collect received: \(response.text)")
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func echoExpand(client: Echo_EchoAsyncClient, message: String) async throws {
+  for try await response in client.expand((.with { $0.text = message })) {
+    print("expand received: \(response.text)")
+  }
+}
+
+@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
+func echoUpdate(client: Echo_EchoAsyncClient, message: String) async throws {
+  let requests = message.components(separatedBy: " ").map { word in
+    Echo_EchoRequest.with { $0.text = word }
+  }
+  for try await response in client.update(requests) {
+    print("update received: \(response.text)")
+  }
+}
+
+// MARK: - "Main"
+
+/// NOTE: We would like to be able to rename this file from `main.swift` to something else and just
+/// use `@main` but I cannot get this to work with the current combination of this repo and Xcode on
+/// macOS.
+import Dispatch
+let dg = DispatchGroup()
+dg.enter()
+if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
+  Task {
+    await Echo.main()
+    dg.leave()
+  }
+} else {
+  print("ERROR: Concurrency only supported on Swift >= 5.5.")
+  dg.leave()
+}
+
+dg.wait()
+
+#else
+
+print("ERROR: Concurrency only supported on Swift >= 5.5.")
+
+#endif

+ 23 - 4
Sources/Examples/Echo/README.md

@@ -4,10 +4,29 @@ This directory contains a simple echo server that demonstrates all four gRPC API
 styles (Unary, Server Streaming, Client Streaming, and Bidirectional Streaming)
 using the gRPC Swift.
 
-There are three subdirectories:
-* `Model` containing the service and model definitions and generated code,
-* `Implementation` containing the server implementation of the generated model,
-* `Runtime` containing a CLI for the server and client.
+There are four subdirectories:
+* `Model/` containing the service and model definitions and generated code,
+* `Implementation/` containing the server implementation of the generated model,
+* `Runtime/` containing a CLI for the server and client using the NIO-based APIs.
+* `AsyncAwaitRuntime/` containing a CLI for the server and client using the
+    async-await–based APIs.
+
+### CLI implementation
+
+The SwiftPM targets for the NIO-based CLI and the async-await–based CLI are
+`Echo` and `AsyncAwaitEcho` respectively.
+
+The below examples make use the former, with commands of the form:
+
+```sh
+swift run Echo ...
+```
+
+To use the CLI using the async-await APIs, replace these commands with:
+
+```sh
+swift run AsyncAwaitEcho ...
+```
 
 ### Server