浏览代码

Refactor the client connection for easier creation. (#477)

* Refactor the client connection for easier creation.

* Typos, nits and tidy ups
George Barnett 6 年之前
父节点
当前提交
46a95c39af

+ 19 - 9
Sources/Examples/Echo/main.swift

@@ -32,21 +32,18 @@ let messageOption = Option("message",
                            default: "Testing 1 2 3",
                            description: "message to send")
 
-func makeClientTLS(enabled: Bool) throws -> GRPCClientConnection.TLSMode {
-  guard enabled else {
-    return .none
-  }
-  return .custom(try NIOSSLContext(configuration: try makeClientTLSConfiguration()))
+func makeClientSSLContext() throws -> NIOSSLContext {
+  return try NIOSSLContext(configuration: makeClientTLSConfiguration())
 }
 
 func makeServerTLS(enabled: Bool) throws -> GRPCServer.TLSMode {
   guard enabled else {
     return .none
   }
-  return .custom(try NIOSSLContext(configuration: try makeServerTLSConfiguration()))
+  return .custom(try NIOSSLContext(configuration: makeServerTLSConfiguration()))
 }
 
-func makeClientTLSConfiguration() throws -> TLSConfiguration {
+func makeClientTLSConfiguration() -> TLSConfiguration {
   let caCert = SampleCertificate.ca
   let clientCert = SampleCertificate.client
   precondition(!caCert.isExpired && !clientCert.isExpired,
@@ -59,7 +56,7 @@ func makeClientTLSConfiguration() throws -> TLSConfiguration {
                     applicationProtocols: GRPCApplicationProtocolIdentifier.allCases.map { $0.rawValue })
 }
 
-func makeServerTLSConfiguration() throws -> TLSConfiguration {
+func makeServerTLSConfiguration() -> TLSConfiguration {
   let caCert = SampleCertificate.ca
   let serverCert = SampleCertificate.server
   precondition(!caCert.isExpired && !serverCert.isExpired,
@@ -74,8 +71,21 @@ func makeServerTLSConfiguration() throws -> TLSConfiguration {
 /// Create en `EchoClient` and wait for it to initialize. Returns nil if initialisation fails.
 func makeEchoClient(address: String, port: Int, ssl: Bool) -> Echo_EchoServiceClient? {
   let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+
   do {
-    return try GRPCClientConnection.start(host: address, port: port, eventLoopGroup: eventLoopGroup, tls: try makeClientTLS(enabled: ssl))
+    let tlsConfiguration: GRPCClientConnection.TLSConfiguration?
+    if ssl {
+      tlsConfiguration = .init(sslContext: try makeClientSSLContext())
+    } else {
+      tlsConfiguration = nil
+    }
+
+    let configuration = GRPCClientConnection.Configuration(
+      target: .hostAndPort(address, port),
+      eventLoopGroup: eventLoopGroup,
+      tlsConfiguration: tlsConfiguration)
+
+    return try GRPCClientConnection.start(configuration)
       .map { Echo_EchoServiceClient(connection: $0) }
       .wait()
   } catch {

+ 12 - 8
Sources/GRPC/ClientCalls/BaseClientCall.swift

@@ -110,11 +110,12 @@ open class BaseClientCall<RequestMessage: Message, ResponseMessage: Message> {
   private func createStreamChannel() {
     self.connection.channel.eventLoop.execute {
       self.connection.multiplexer.createStreamChannel(promise: self.streamPromise) { (subchannel, streamID) -> EventLoopFuture<Void> in
-        subchannel.pipeline.addHandlers(HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: self.connection.httpProtocol),
-                                        HTTP1ToRawGRPCClientCodec(),
-                                        GRPCClientCodec<RequestMessage, ResponseMessage>(),
-                                        self.requestHandler,
-                                        self.responseHandler)
+        subchannel.pipeline.addHandlers(
+          HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: self.connection.configuration.httpProtocol),
+          HTTP1ToRawGRPCClientCodec(),
+          GRPCClientCodec<RequestMessage, ResponseMessage>(),
+          self.requestHandler,
+          self.responseHandler)
       }
     }
   }
@@ -141,10 +142,8 @@ extension BaseClientCall: ClientCall {
 /// - Parameter path: The path of the gRPC call, e.g. "/serviceName/methodName".
 /// - Parameter host: The host serving the call.
 /// - Parameter callOptions: Options used when making this call.
-internal func makeRequestHead(path: String, host: String, callOptions: CallOptions) -> HTTPRequestHead {
+internal func makeRequestHead(path: String, host: String?, callOptions: CallOptions) -> HTTPRequestHead {
   var headers: HTTPHeaders = [
-    // We're dealing with HTTP/1; the NIO HTTP2ToHTTP1Codec replaces "host" with ":authority".
-    "host": host,
     "content-type": "application/grpc",
     // Used to detect incompatible proxies, as per https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
     "te": "trailers",
@@ -153,6 +152,11 @@ internal func makeRequestHead(path: String, host: String, callOptions: CallOptio
     GRPCHeaderName.acceptEncoding: CompressionMechanism.acceptEncodingHeader,
   ]
 
+  if let host = host {
+    // We're dealing with HTTP/1; the NIO HTTP2ToHTTP1Codec replaces "host" with ":authority".
+    headers.add(name: "host", value: host)
+  }
+
   if callOptions.timeout != .infinite {
     headers.add(name: GRPCHeaderName.timeout, value: String(describing: callOptions.timeout))
   }

+ 1 - 1
Sources/GRPC/ClientCalls/BidirectionalStreamingClientCall.swift

@@ -42,7 +42,7 @@ public final class BidirectionalStreamingClientCall<RequestMessage: Message, Res
       responseHandler: handler)
 
     let requestHandler = GRPCClientStreamingRequestChannelHandler<RequestMessage>(
-      requestHead: makeRequestHead(path: path, host: connection.host, callOptions: callOptions))
+      requestHead: makeRequestHead(path: path, host: connection.configuration.target.host, callOptions: callOptions))
 
     super.init(
       connection: connection,

+ 1 - 1
Sources/GRPC/ClientCalls/ClientStreamingClientCall.swift

@@ -43,7 +43,7 @@ public final class ClientStreamingClientCall<RequestMessage: Message, ResponseMe
       timeout: callOptions.timeout)
 
     let requestHandler = GRPCClientStreamingRequestChannelHandler<RequestMessage>(
-      requestHead: makeRequestHead(path: path, host: connection.host, callOptions: callOptions))
+      requestHead: makeRequestHead(path: path, host: connection.configuration.target.host, callOptions: callOptions))
 
     self.response = responseHandler.responsePromise.futureResult
     self.messageQueue = connection.channel.eventLoop.makeSucceededFuture(())

+ 1 - 1
Sources/GRPC/ClientCalls/ServerStreamingClientCall.swift

@@ -33,7 +33,7 @@ public final class ServerStreamingClientCall<RequestMessage: Message, ResponseMe
       responseHandler: handler)
 
     let requestHandler = GRPCClientUnaryRequestChannelHandler<RequestMessage>(
-      requestHead: makeRequestHead(path: path, host: connection.host, callOptions: callOptions),
+      requestHead: makeRequestHead(path: path, host: connection.configuration.target.host, callOptions: callOptions),
       request: _Box(request))
 
     super.init(

+ 1 - 1
Sources/GRPC/ClientCalls/UnaryClientCall.swift

@@ -40,7 +40,7 @@ public final class UnaryClientCall<RequestMessage: Message, ResponseMessage: Mes
       timeout: callOptions.timeout)
 
     let requestHandler = GRPCClientUnaryRequestChannelHandler<RequestMessage>(
-      requestHead: makeRequestHead(path: path, host: connection.host, callOptions: callOptions),
+      requestHead: makeRequestHead(path: path, host: connection.configuration.target.host, callOptions: callOptions),
       request: _Box(request))
 
     self.response = responseHandler.responsePromise.futureResult

+ 4 - 4
Sources/GRPC/GRPCClient.swift

@@ -32,7 +32,7 @@ extension GRPCClient {
     callOptions: CallOptions,
     responseType: Response.Type = Response.self
   ) -> UnaryClientCall<Request, Response> {
-    return UnaryClientCall(connection: self.connection, path: path, request: request, callOptions: callOptions, errorDelegate: self.connection.errorDelegate)
+    return UnaryClientCall(connection: self.connection, path: path, request: request, callOptions: callOptions, errorDelegate: self.connection.configuration.errorDelegate)
   }
 
   public func makeServerStreamingCall<Request: Message, Response: Message>(
@@ -42,7 +42,7 @@ extension GRPCClient {
     responseType: Response.Type = Response.self,
     handler: @escaping (Response) -> Void
   ) -> ServerStreamingClientCall<Request, Response> {
-    return ServerStreamingClientCall(connection: self.connection, path: path, request: request, callOptions: callOptions, errorDelegate: self.connection.errorDelegate, handler: handler)
+    return ServerStreamingClientCall(connection: self.connection, path: path, request: request, callOptions: callOptions, errorDelegate: self.connection.configuration.errorDelegate, handler: handler)
   }
 
   public func makeClientStreamingCall<Request: Message, Response: Message>(
@@ -51,7 +51,7 @@ extension GRPCClient {
     requestType: Request.Type = Request.self,
     responseType: Response.Type = Response.self
   ) -> ClientStreamingClientCall<Request, Response> {
-    return ClientStreamingClientCall(connection: self.connection, path: path, callOptions: callOptions, errorDelegate: self.connection.errorDelegate)
+    return ClientStreamingClientCall(connection: self.connection, path: path, callOptions: callOptions, errorDelegate: self.connection.configuration.errorDelegate)
   }
 
   public func makeBidirectionalStreamingCall<Request: Message, Response: Message>(
@@ -61,7 +61,7 @@ extension GRPCClient {
     responseType: Response.Type = Response.self,
     handler: @escaping (Response) -> Void
   ) -> BidirectionalStreamingClientCall<Request, Response> {
-    return BidirectionalStreamingClientCall(connection: self.connection, path: path, callOptions: callOptions, errorDelegate: self.connection.errorDelegate, handler: handler)
+    return BidirectionalStreamingClientCall(connection: self.connection, path: path, callOptions: callOptions, errorDelegate: self.connection.configuration.errorDelegate, handler: handler)
   }
 }
 

+ 216 - 81
Sources/GRPC/GRPCClientConnection.swift

@@ -54,111 +54,90 @@ import NIOTLS
 ///
 /// See `BaseClientCall` for a description of the remainder of the client pipeline.
 open class GRPCClientConnection {
-  /// Starts a connection to the given host and port.
+  /// Makes and configures a `ClientBootstrap` using the provided configuration.
   ///
-  /// - Parameters:
-  ///   - host: Host to connect to.
-  ///   - port: Port on the host to connect to.
-  ///   - eventLoopGroup: Event loop group to run the connection on.
-  ///   - errorDelegate: An error delegate which is called when errors are caught. Provided
-  ///       delegates **must not maintain a strong reference to this `GRPCClientConnection`**. Doing
-  ///       so will cause a retain cycle. Defaults to a delegate which logs errors in debug builds
-  ///       only.
-  ///   - tlsMode: How TLS should be configured for this connection.
-  ///   - hostOverride: Value to use for TLS SNI extension; this must not be an IP address. Ignored
-  ///       if `tlsMode` is `.none`.
-  /// - Returns: A future which will be fulfilled with a connection to the remote peer.
-  public static func start(
-    host: String,
-    port: Int,
-    eventLoopGroup: EventLoopGroup,
-    errorDelegate: ClientErrorDelegate? = DebugOnlyLoggingClientErrorDelegate.shared,
-    tls tlsMode: TLSMode = .none,
-    hostOverride: String? = nil
-  ) throws -> EventLoopFuture<GRPCClientConnection> {
-    let bootstrap = ClientBootstrap(group: eventLoopGroup)
+  /// Enables `SO_REUSEADDR` and `TCP_NODELAY` and configures the `channelInitializer` to use the
+  /// handlers detailed in the documentation for `GRPCClientConnection`.
+  ///
+  /// - Parameter configuration: The configuration to prepare the bootstrap with.
+  public class func makeBootstrap(configuration: Configuration) -> ClientBootstrap {
+    let bootstrap = ClientBootstrap(group: configuration.eventLoopGroup)
       // Enable SO_REUSEADDR and TCP_NODELAY.
       .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
       .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
       .channelInitializer { channel in
-        configureTLS(mode: tlsMode, channel: channel, host: hostOverride ?? host, errorDelegate: errorDelegate).flatMap {
+        let tlsConfigured = configuration.tlsConfiguration.map { tlsConfiguration in
+          channel.configureTLS(tlsConfiguration, errorDelegate: configuration.errorDelegate)
+        }
+
+        return (tlsConfigured ?? channel.eventLoop.makeSucceededFuture(())).flatMap {
           channel.configureHTTP2Pipeline(mode: .client)
         }.flatMap { _ in
-          channel.pipeline.addHandler(GRPCDelegatingErrorHandler(delegate: errorDelegate))
+          let errorHandler = GRPCDelegatingErrorHandler(delegate: configuration.errorDelegate)
+          return channel.pipeline.addHandler(errorHandler)
         }
       }
 
-    return bootstrap.connect(host: host, port: port).flatMap { channel in
-      // Check the handshake succeeded and a valid protocol was negotiated via ALPN.
-      let tlsVerified: EventLoopFuture<Void>
-
-      if case .none = tlsMode {
-        tlsVerified = channel.eventLoop.makeSucceededFuture(())
-      } else {
-        // TODO: Use `handler(type:)` introduced in https://github.com/apple/swift-nio/pull/974
-        // once it has been released.
-        tlsVerified = channel.pipeline.context(handlerType: GRPCTLSVerificationHandler.self).map {
-          $0.handler as! GRPCTLSVerificationHandler
-        }.flatMap {
-          // Use the result of the verification future to determine whether we should return a
-          // connection to the caller. Note that even though it contains a `Void` it may also
-          // contain an `Error`, which is what we are interested in here.
-          $0.verification
-        }
-      }
+    return bootstrap
+  }
 
-      return tlsVerified.flatMap {
-        // TODO: Use `handler(type:)` introduced in https://github.com/apple/swift-nio/pull/974
-        // once it has been released.
-        channel.pipeline.context(handlerType: HTTP2StreamMultiplexer.self)
-      }.map {
-        $0.handler as! HTTP2StreamMultiplexer
-      }.map { multiplexer in
-        GRPCClientConnection(channel: channel, multiplexer: multiplexer, host: host, httpProtocol: tlsMode.httpProtocol, errorDelegate: errorDelegate)
-      }
+  /// Verifies that a TLS handshake was successful by using the `GRPCTLSVerificationHandler`.
+  ///
+  /// - Parameter channel: The channel to verify successful TLS setup on.
+  public class func verifyTLS(channel: Channel) -> EventLoopFuture<Void> {
+    return channel.pipeline.handler(type: GRPCTLSVerificationHandler.self).flatMap {
+      $0.verification
     }
   }
 
-  /// Configure an SSL handler on the channel, if one is required.
+  /// Makes a `GRPCClientConnection` from the given channel and configuration.
   ///
-  /// - Parameters:
-  ///   - mode: TLS mode to use when creating the new handler.
-  ///   - channel: The channel on which to add the SSL handler.
-  ///   - host: The hostname of the server we're connecting to.
-  ///   - errorDelegate: The error delegate to use.
-  /// - Returns: A future which will be succeeded when the pipeline has been configured.
-  private static func configureTLS(mode tls: TLSMode, channel: Channel, host: String, errorDelegate: ClientErrorDelegate?) -> EventLoopFuture<Void> {
-    let handlerAddedPromise: EventLoopPromise<Void> = channel.eventLoop.makePromise()
-
-    do {
-      guard let sslContext = try tls.makeSSLContext() else {
-        handlerAddedPromise.succeed(())
-        return handlerAddedPromise.futureResult
-      }
-
-      let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: host)
-      let verificationHandler = GRPCTLSVerificationHandler(errorDelegate: errorDelegate)
-
-      channel.pipeline.addHandlers(sslHandler, verificationHandler).cascade(to: handlerAddedPromise)
-    } catch {
-      handlerAddedPromise.fail(error)
+  /// - Parameter channel: The channel to use for the connection.
+  /// - Parameter configuration: The configuration used to create the channel.
+  public class func makeGRPCClientConnection(
+    channel: Channel,
+    configuration: Configuration
+  ) -> EventLoopFuture<GRPCClientConnection> {
+    return channel.pipeline.handler(type: HTTP2StreamMultiplexer.self).map { multiplexer in
+      GRPCClientConnection(channel: channel, multiplexer: multiplexer, configuration: configuration)
     }
+  }
 
-    return handlerAddedPromise.futureResult
+  /// Starts a client connection using the given configuration.
+  ///
+  /// This involves: creating a `ClientBootstrap`, connecting to a target, verifying that the TLS
+  /// handshake was successful (if TLS was configured) and creating the `GRPCClientConnection`.
+  /// See the individual functions for more information:
+  ///  - `makeBootstrap(configuration:)`,
+  ///  - `verifyTLS(channel:)`, and
+  ///  - `makeGRPCClientConnection(channel:configuration:)`.
+  ///
+  /// - Parameter configuration: The configuration to start the connection with.
+  public class func start(_ configuration: Configuration) -> EventLoopFuture<GRPCClientConnection> {
+    return makeBootstrap(configuration: configuration)
+      .connect(to: configuration.target)
+      .flatMap { channel in
+        let tlsVerified: EventLoopFuture<Void>?
+        if configuration.tlsConfiguration != nil {
+          tlsVerified = verifyTLS(channel: channel)
+        } else {
+          tlsVerified = nil
+        }
+
+        return (tlsVerified ?? channel.eventLoop.makeSucceededFuture(())).flatMap {
+          makeGRPCClientConnection(channel: channel, configuration: configuration)
+        }
+      }
   }
 
   public let channel: Channel
   public let multiplexer: HTTP2StreamMultiplexer
-  public let host: String
-  public let httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol
-  public let errorDelegate: ClientErrorDelegate?
+  public let configuration: Configuration
 
-  init(channel: Channel, multiplexer: HTTP2StreamMultiplexer, host: String, httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol, errorDelegate: ClientErrorDelegate?) {
+  init(channel: Channel, multiplexer: HTTP2StreamMultiplexer, configuration: Configuration) {
     self.channel = channel
     self.multiplexer = multiplexer
-    self.host = host
-    self.httpProtocol = httpProtocol
-    self.errorDelegate = errorDelegate
+    self.configuration = configuration
   }
 
   /// Fired when the client shuts down.
@@ -171,7 +150,163 @@ open class GRPCClientConnection {
   }
 }
 
+// MARK: - Configuration structures
+
+/// A target to connect to.
+public enum ConnectionTarget {
+  /// The host and port.
+  case hostAndPort(String, Int)
+  /// The path of a Unix domain socket.
+  case unixDomainSocket(String)
+  /// A NIO socket address.
+  case socketAddress(SocketAddress)
+
+  var host: String? {
+    guard case .hostAndPort(let host, _) = self else {
+      return nil
+    }
+    return host
+  }
+}
+
 extension GRPCClientConnection {
+  /// The configuration for a connection.
+  public struct Configuration {
+    /// The target to connect to.
+    public var target: ConnectionTarget
+
+    /// The event loop group to run the connection on.
+    public var eventLoopGroup: EventLoopGroup
+
+    /// An error delegate which is called when errors are caught. Provided delegates **must not
+    /// maintain a strong reference to this `GRPCClientConnection`**. Doing so will cause a retain
+    /// cycle.
+    public var errorDelegate: ClientErrorDelegate?
+
+    /// TLS configuration for this connection. `nil` if TLS is not desired.
+    public var tlsConfiguration: TLSConfiguration?
+
+    /// The HTTP protocol used for this connection.
+    public var httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol {
+      return self.tlsConfiguration == nil ? .http : .https
+    }
+
+    /// Create a `Configuration` with some pre-defined defaults.
+    ///
+    /// - Parameter target: The target to connect to.
+    /// - Parameter eventLoopGroup: The event loop group to run the connection on.
+    /// - Parameter errorDelegate: The error delegate, defaulting to a delegate which will log only
+    ///     on debug builds.
+    /// - Parameter tlsConfiguration: TLS configuration, defaulting to `nil`.
+    public init(
+      target: ConnectionTarget,
+      eventLoopGroup: EventLoopGroup,
+      errorDelegate: ClientErrorDelegate? = DebugOnlyLoggingClientErrorDelegate.shared,
+      tlsConfiguration: TLSConfiguration? = nil
+      ) {
+      self.target = target
+      self.eventLoopGroup = eventLoopGroup
+      self.errorDelegate = errorDelegate
+      self.tlsConfiguration = tlsConfiguration
+    }
+  }
+
+  /// The TLS configuration for a connection.
+  public struct TLSConfiguration {
+    /// The SSL context to use.
+    public var sslContext: NIOSSLContext
+    /// Value to use for TLS SNI extension; this must not be an IP address.
+    public var hostnameOverride: String?
+
+    public init(sslContext: NIOSSLContext, hostnameOverride: String? = nil) {
+      self.sslContext = sslContext
+      self.hostnameOverride = hostnameOverride
+    }
+  }
+}
+
+// MARK: - Configuration helpers/extensions
+
+fileprivate extension ClientBootstrap {
+  /// Connect to the given connection target.
+  ///
+  /// - Parameter target: The target to connect to.
+  func connect(to target: ConnectionTarget) -> EventLoopFuture<Channel> {
+    switch target {
+    case .hostAndPort(let host, let port):
+      return self.connect(host: host, port: port)
+
+    case .unixDomainSocket(let path):
+      return self.connect(unixDomainSocketPath: path)
+
+    case .socketAddress(let address):
+      return self.connect(to: address)
+    }
+  }
+}
+
+fileprivate extension Channel {
+  /// Configure the channel with TLS.
+  ///
+  /// This function adds two handlers to the pipeline: the `NIOSSLClientHandler` to handle TLS, and
+  /// the `GRPCTLSVerificationHandler` which verifies that a successful handshake was completed.
+  ///
+  /// - Parameter configuration: The configuration to configure the channel with.
+  /// - Parameter errorDelegate: The error delegate to use for the TLS verification handler.
+  func configureTLS(
+    _ configuration: GRPCClientConnection.TLSConfiguration,
+    errorDelegate: ClientErrorDelegate?
+    ) -> EventLoopFuture<Void> {
+    do {
+      let sslClientHandler = try NIOSSLClientHandler(
+        context: configuration.sslContext,
+        serverHostname: configuration.hostnameOverride)
+
+      let verificationHandler = GRPCTLSVerificationHandler(errorDelegate: errorDelegate)
+      return self.pipeline.addHandlers(sslClientHandler, verificationHandler)
+    } catch {
+      return self.eventLoop.makeFailedFuture(error)
+    }
+  }
+}
+
+// MARK: - Legacy APIs
+
+extension GRPCClientConnection {
+  /// Starts a connection to the given host and port.
+  ///
+  /// - Parameters:
+  ///   - host: Host to connect to.
+  ///   - port: Port on the host to connect to.
+  ///   - eventLoopGroup: Event loop group to run the connection on.
+  ///   - errorDelegate: An error delegate which is called when errors are caught. Provided
+  ///       delegates **must not maintain a strong reference to this `GRPCClientConnection`**. Doing
+  ///       so will cause a retain cycle. Defaults to a delegate which logs errors in debug builds
+  ///       only.
+  ///   - tlsMode: How TLS should be configured for this connection.
+  ///   - hostOverride: Value to use for TLS SNI extension; this must not be an IP address. Ignored
+  ///       if `tlsMode` is `.none`.
+  /// - Returns: A future which will be fulfilled with a connection to the remote peer.
+  public static func start(
+    host: String,
+    port: Int,
+    eventLoopGroup: EventLoopGroup,
+    errorDelegate: ClientErrorDelegate? = DebugOnlyLoggingClientErrorDelegate.shared,
+    tls tlsMode: TLSMode = .none,
+    hostOverride: String? = nil
+  ) throws -> EventLoopFuture<GRPCClientConnection> {
+    var configuration = Configuration(
+      target: .hostAndPort(host, port),
+      eventLoopGroup: eventLoopGroup,
+      errorDelegate: errorDelegate)
+
+    if let sslContext = try tlsMode.makeSSLContext() {
+      configuration.tlsConfiguration = .init(sslContext: sslContext, hostnameOverride: hostOverride)
+    }
+
+    return GRPCClientConnection.start(configuration)
+  }
+
   public enum TLSMode {
     case none
     case anonymous