HTTP2ClientTransport+Posix.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. public import GRPCCore
  17. public import GRPCNIOTransportCore // should be @usableFromInline
  18. public import NIOCore // has to be public because of EventLoopGroup param in init
  19. public import NIOPosix // has to be public because of default argument value in init
  20. #if canImport(NIOSSL)
  21. private import NIOSSL
  22. #endif
  23. extension HTTP2ClientTransport {
  24. /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`.
  25. ///
  26. /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use
  27. /// on Linux and Darwin based platforms (macOS, iOS, etc.). However, it's *strongly* recommended
  28. /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of
  29. /// the `HTTP2ClientTransport`.
  30. ///
  31. /// To use this transport you need to provide a 'target' to connect to which will be resolved
  32. /// by an appropriate resolver from the resolver registry. By default the resolver registry can
  33. /// resolve DNS targets, IPv4 and IPv6 targets, Unix domain socket targets, and Virtual Socket
  34. /// targets. If you use a custom target you must also provide an appropriately configured
  35. /// registry.
  36. ///
  37. /// You can control various aspects of connection creation, management, security and RPC behavior via
  38. /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via
  39. /// the `ServiceConfig` (if it isn't provided by a resolver).
  40. ///
  41. /// Beyond creating the transport you don't need to interact with it directly, instead, pass it
  42. /// to a `GRPCClient`:
  43. ///
  44. /// ```swift
  45. /// try await withThrowingDiscardingTaskGroup { group in
  46. /// let transport = try HTTP2ClientTransport.Posix(
  47. /// target: .ipv4(host: "example.com"),
  48. /// config: .defaults(transportSecurity: .plaintext)
  49. /// )
  50. /// let client = GRPCClient(transport: transport)
  51. /// group.addTask {
  52. /// try await client.run()
  53. /// }
  54. ///
  55. /// // ...
  56. /// }
  57. /// ```
  58. public struct Posix: ClientTransport {
  59. private let channel: GRPCChannel
  60. /// Creates a new NIOPosix-based HTTP/2 client transport.
  61. ///
  62. /// - Parameters:
  63. /// - target: A target to resolve.
  64. /// - config: Configuration for the transport.
  65. /// - resolverRegistry: A registry of resolver factories.
  66. /// - serviceConfig: Service config controlling how the transport should establish and
  67. /// load-balance connections.
  68. /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must
  69. /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from
  70. /// a `MultiThreadedEventLoopGroup`.
  71. /// - Throws: When no suitable resolver could be found for the `target`.
  72. public init(
  73. target: any ResolvableTarget,
  74. config: Config,
  75. resolverRegistry: NameResolverRegistry = .defaults,
  76. serviceConfig: ServiceConfig = ServiceConfig(),
  77. eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup
  78. ) throws {
  79. guard let resolver = resolverRegistry.makeResolver(for: target) else {
  80. throw RuntimeError(
  81. code: .transportError,
  82. message: """
  83. No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \
  84. registry has a suitable name resolver factory registered for the given target.
  85. """
  86. )
  87. }
  88. self.channel = GRPCChannel(
  89. resolver: resolver,
  90. connector: try Connector(eventLoopGroup: eventLoopGroup, config: config),
  91. config: GRPCChannel.Config(posix: config),
  92. defaultServiceConfig: serviceConfig
  93. )
  94. }
  95. public var retryThrottle: RetryThrottle? {
  96. self.channel.retryThrottle
  97. }
  98. public func connect() async {
  99. await self.channel.connect()
  100. }
  101. public func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? {
  102. self.channel.config(forMethod: descriptor)
  103. }
  104. public func beginGracefulShutdown() {
  105. self.channel.beginGracefulShutdown()
  106. }
  107. public func withStream<T: Sendable>(
  108. descriptor: MethodDescriptor,
  109. options: CallOptions,
  110. _ closure: (RPCStream<Inbound, Outbound>) async throws -> T
  111. ) async throws -> T {
  112. try await self.channel.withStream(descriptor: descriptor, options: options, closure)
  113. }
  114. }
  115. }
  116. extension HTTP2ClientTransport.Posix {
  117. struct Connector: HTTP2Connector {
  118. private let config: HTTP2ClientTransport.Posix.Config
  119. private let eventLoopGroup: any EventLoopGroup
  120. #if canImport(NIOSSL)
  121. private let nioSSLContext: NIOSSLContext?
  122. private let serverHostname: String?
  123. #endif
  124. init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws {
  125. self.eventLoopGroup = eventLoopGroup
  126. self.config = config
  127. #if canImport(NIOSSL)
  128. switch self.config.transportSecurity.wrapped {
  129. case .plaintext:
  130. self.nioSSLContext = nil
  131. self.serverHostname = nil
  132. case .tls(let tlsConfig):
  133. do {
  134. self.nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig))
  135. self.serverHostname = tlsConfig.serverHostname
  136. } catch {
  137. throw RuntimeError(
  138. code: .transportError,
  139. message: "Couldn't create SSL context, check your TLS configuration.",
  140. cause: error
  141. )
  142. }
  143. }
  144. #endif
  145. }
  146. func establishConnection(
  147. to address: GRPCNIOTransportCore.SocketAddress
  148. ) async throws -> HTTP2Connection {
  149. let (channel, multiplexer) = try await ClientBootstrap(
  150. group: self.eventLoopGroup
  151. ).connect(to: address) { channel in
  152. channel.eventLoop.makeCompletedFuture {
  153. #if canImport(NIOSSL)
  154. if let nioSSLContext = self.nioSSLContext {
  155. try channel.pipeline.syncOperations.addHandler(
  156. NIOSSLClientHandler(
  157. context: nioSSLContext,
  158. serverHostname: self.serverHostname
  159. )
  160. )
  161. }
  162. #endif
  163. return try channel.pipeline.syncOperations.configureGRPCClientPipeline(
  164. channel: channel,
  165. config: GRPCChannel.Config(posix: self.config)
  166. )
  167. }
  168. }
  169. return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: true)
  170. }
  171. }
  172. }
  173. extension HTTP2ClientTransport.Posix {
  174. public struct Config: Sendable {
  175. /// Configuration for HTTP/2 connections.
  176. public var http2: HTTP2ClientTransport.Config.HTTP2
  177. /// Configuration for backoff used when establishing a connection.
  178. public var backoff: HTTP2ClientTransport.Config.Backoff
  179. /// Configuration for connection management.
  180. public var connection: HTTP2ClientTransport.Config.Connection
  181. /// Compression configuration.
  182. public var compression: HTTP2ClientTransport.Config.Compression
  183. /// The transport's security.
  184. public var transportSecurity: TransportSecurity
  185. /// Creates a new connection configuration.
  186. ///
  187. /// - Parameters:
  188. /// - http2: HTTP2 configuration.
  189. /// - backoff: Backoff configuration.
  190. /// - connection: Connection configuration.
  191. /// - compression: Compression configuration.
  192. /// - transportSecurity: The transport's security configuration.
  193. ///
  194. /// - SeeAlso: ``defaults(transportSecurity:configure:)``
  195. public init(
  196. http2: HTTP2ClientTransport.Config.HTTP2,
  197. backoff: HTTP2ClientTransport.Config.Backoff,
  198. connection: HTTP2ClientTransport.Config.Connection,
  199. compression: HTTP2ClientTransport.Config.Compression,
  200. transportSecurity: TransportSecurity
  201. ) {
  202. self.http2 = http2
  203. self.connection = connection
  204. self.backoff = backoff
  205. self.compression = compression
  206. self.transportSecurity = transportSecurity
  207. }
  208. /// Default values.
  209. ///
  210. /// - Parameters:
  211. /// - transportSecurity: The security settings applied to the transport.
  212. /// - configure: A closure which allows you to modify the defaults before returning them.
  213. public static func defaults(
  214. transportSecurity: TransportSecurity,
  215. configure: (_ config: inout Self) -> Void = { _ in }
  216. ) -> Self {
  217. var config = Self(
  218. http2: .defaults,
  219. backoff: .defaults,
  220. connection: .defaults,
  221. compression: .defaults,
  222. transportSecurity: transportSecurity
  223. )
  224. configure(&config)
  225. return config
  226. }
  227. }
  228. }
  229. extension GRPCChannel.Config {
  230. init(posix: HTTP2ClientTransport.Posix.Config) {
  231. self.init(
  232. http2: posix.http2,
  233. backoff: posix.backoff,
  234. connection: posix.connection,
  235. compression: posix.compression
  236. )
  237. }
  238. }
  239. extension ClientTransport where Self == HTTP2ClientTransport.Posix {
  240. /// Creates a new Posix based HTTP/2 client transport.
  241. ///
  242. /// - Parameters:
  243. /// - target: A target to resolve.
  244. /// - config: Configuration for the transport.
  245. /// - resolverRegistry: A registry of resolver factories.
  246. /// - serviceConfig: Service config controlling how the transport should establish and
  247. /// load-balance connections.
  248. /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must
  249. /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from
  250. /// a `MultiThreadedEventLoopGroup`.
  251. /// - Throws: When no suitable resolver could be found for the `target`.
  252. public static func http2NIOPosix(
  253. target: any ResolvableTarget,
  254. config: HTTP2ClientTransport.Posix.Config,
  255. resolverRegistry: NameResolverRegistry = .defaults,
  256. serviceConfig: ServiceConfig = ServiceConfig(),
  257. eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup
  258. ) throws -> Self {
  259. return try HTTP2ClientTransport.Posix(
  260. target: target,
  261. config: config,
  262. resolverRegistry: resolverRegistry,
  263. serviceConfig: serviceConfig,
  264. eventLoopGroup: eventLoopGroup
  265. )
  266. }
  267. }