Browse Source

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

Si Beaumont 4 years ago
parent
commit
bbfd50bf76

+ 14 - 0
Package.swift

@@ -174,6 +174,20 @@ let package = Package(
       path: "Sources/Examples/Echo/Runtime"
       path: "Sources/Examples/Echo/Runtime"
     ),
     ),
 
 
+    // Echo example CLI using async-await API.
+    .target(
+      name: "AsyncAwaitEcho",
+      dependencies: [
+        .target(name: "EchoModel"),
+        .target(name: "EchoImplementation"),
+        .target(name: "GRPC"),
+        .target(name: "GRPCSampleData"),
+        .product(name: "SwiftProtobuf", package: "SwiftProtobuf"),
+        .product(name: "ArgumentParser", package: "swift-argument-parser"),
+      ],
+      path: "Sources/Examples/Echo/AsyncAwaitRuntime"
+    ),
+
     // Echo example service implementation.
     // Echo example service implementation.
     .target(
     .target(
       name: "EchoImplementation",
       name: "EchoImplementation",

+ 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

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

@@ -0,0 +1,261 @@
+/*
+ * 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 NIO
+
+// 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 {
+    // 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 {
+    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 {
+    // 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 {
+    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)
 styles (Unary, Server Streaming, Client Streaming, and Bidirectional Streaming)
 using the gRPC Swift.
 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
 ### Server