Serve.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. import ArgumentParser
  17. import Foundation
  18. import GRPCHTTP2Transport
  19. import GRPCProtobuf
  20. import Synchronization
  21. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
  22. struct Serve: AsyncParsableCommand {
  23. static let configuration = CommandConfiguration(abstract: "Starts a route-guide server.")
  24. @Option(help: "The port to listen on")
  25. var port: Int = 31415
  26. private func loadFeatures() throws -> [Routeguide_Feature] {
  27. guard let url = Bundle.module.url(forResource: "route_guide_db", withExtension: "json") else {
  28. throw RPCError(code: .internalError, message: "Couldn't find 'route_guide_db.json")
  29. }
  30. let data = try Data(contentsOf: url)
  31. return try Routeguide_Feature.array(fromJSONUTF8Bytes: data)
  32. }
  33. func run() async throws {
  34. let features = try self.loadFeatures()
  35. let transport = HTTP2ServerTransport.Posix(
  36. address: .ipv4(host: "127.0.0.1", port: self.port),
  37. config: .defaults(transportSecurity: .plaintext)
  38. )
  39. let server = GRPCServer(transport: transport, services: [RouteGuideService(features: features)])
  40. try await withThrowingDiscardingTaskGroup { group in
  41. group.addTask { try await server.serve() }
  42. let address = try await transport.listeningAddress
  43. print("server listening on \(address)")
  44. }
  45. }
  46. }
  47. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
  48. struct RouteGuideService {
  49. /// Known features.
  50. private let features: [Routeguide_Feature]
  51. /// Notes recorded by clients.
  52. private let receivedNotes: Notes
  53. /// A thread-safe store for notes sent by clients.
  54. private final class Notes: Sendable {
  55. private let notes: Mutex<[Routeguide_RouteNote]>
  56. init() {
  57. self.notes = Mutex([])
  58. }
  59. /// Records a note and returns all other notes recorded at the same location.
  60. ///
  61. /// - Parameter receivedNote: A note to record.
  62. /// - Returns: Other notes recorded at the same location.
  63. func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] {
  64. return self.notes.withLock { notes in
  65. var notesFromSameLocation: [Routeguide_RouteNote] = []
  66. for note in notes {
  67. if note.location == receivedNote.location {
  68. notesFromSameLocation.append(note)
  69. }
  70. }
  71. notes.append(receivedNote)
  72. return notesFromSameLocation
  73. }
  74. }
  75. }
  76. /// Creates a new route guide service.
  77. /// - Parameter features: Known features.
  78. init(features: [Routeguide_Feature]) {
  79. self.features = features
  80. self.receivedNotes = Notes()
  81. }
  82. /// Returns the first feature found at the given location, if one exists.
  83. private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? {
  84. self.features.first {
  85. $0.location.latitude == latitude && $0.location.longitude == longitude
  86. }
  87. }
  88. }
  89. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
  90. extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol {
  91. func getFeature(
  92. request: ServerRequest.Single<Routeguide_Point>
  93. ) async throws -> ServerResponse.Single<Routeguide_Feature> {
  94. let feature = self.findFeature(
  95. latitude: request.message.latitude,
  96. longitude: request.message.longitude
  97. )
  98. if let feature {
  99. return ServerResponse.Single(message: feature)
  100. } else {
  101. // No feature: return a feature with an empty name.
  102. let unknownFeature = Routeguide_Feature.with {
  103. $0.name = ""
  104. $0.location = .with {
  105. $0.latitude = request.message.latitude
  106. $0.longitude = request.message.longitude
  107. }
  108. }
  109. return ServerResponse.Single(message: unknownFeature)
  110. }
  111. }
  112. func listFeatures(
  113. request: ServerRequest.Single<Routeguide_Rectangle>
  114. ) async throws -> ServerResponse.Stream<Routeguide_Feature> {
  115. return ServerResponse.Stream { writer in
  116. let featuresWithinBounds = self.features.filter { feature in
  117. !feature.name.isEmpty && feature.isContained(by: request.message)
  118. }
  119. try await writer.write(contentsOf: featuresWithinBounds)
  120. return [:]
  121. }
  122. }
  123. func recordRoute(
  124. request: ServerRequest.Stream<Routeguide_Point>
  125. ) async throws -> ServerResponse.Single<Routeguide_RouteSummary> {
  126. let startTime = ContinuousClock.now
  127. var pointsVisited = 0
  128. var featuresVisited = 0
  129. var distanceTravelled = 0.0
  130. var previousPoint: Routeguide_Point? = nil
  131. for try await point in request.messages {
  132. pointsVisited += 1
  133. if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil {
  134. featuresVisited += 1
  135. }
  136. if let previousPoint {
  137. distanceTravelled += greatCircleDistance(from: previousPoint, to: point)
  138. }
  139. previousPoint = point
  140. }
  141. let duration = startTime.duration(to: .now)
  142. let summary = Routeguide_RouteSummary.with {
  143. $0.pointCount = Int32(pointsVisited)
  144. $0.featureCount = Int32(featuresVisited)
  145. $0.elapsedTime = Int32(duration.components.seconds)
  146. $0.distance = Int32(distanceTravelled)
  147. }
  148. return ServerResponse.Single(message: summary)
  149. }
  150. func routeChat(
  151. request: ServerRequest.Stream<Routeguide_RouteNote>
  152. ) async throws -> ServerResponse.Stream<Routeguide_RouteNote> {
  153. return ServerResponse.Stream { writer in
  154. for try await note in request.messages {
  155. let notes = self.receivedNotes.recordNote(note)
  156. try await writer.write(contentsOf: notes)
  157. }
  158. return [:]
  159. }
  160. }
  161. }
  162. extension Routeguide_Feature {
  163. func isContained(
  164. by rectangle: Routeguide_Rectangle
  165. ) -> Bool {
  166. return rectangle.lo.latitude <= self.location.latitude
  167. && self.location.latitude <= rectangle.hi.latitude
  168. && rectangle.lo.longitude <= self.location.longitude
  169. && self.location.longitude <= rectangle.hi.longitude
  170. }
  171. }
  172. private func greatCircleDistance(
  173. from point1: Routeguide_Point,
  174. to point2: Routeguide_Point
  175. ) -> Double {
  176. // See https://en.wikipedia.org/wiki/Great-circle_distance
  177. //
  178. // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1.
  179. // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2.
  180. //
  181. // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1.
  182. //
  183. // The central angle between them, σc (sigmaC) can be computed as:
  184. //
  185. // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2))
  186. //
  187. // The unit distance (d) between point1 and point2 can then be computed as:
  188. //
  189. // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc))
  190. let lambda1 = radians(degreesInE7: point1.longitude)
  191. let phi1 = radians(degreesInE7: point1.latitude)
  192. let lambda2 = radians(degreesInE7: point2.longitude)
  193. let phi2 = radians(degreesInE7: point2.latitude)
  194. // Δλ = λ2 - λ1
  195. let deltaLambda = lambda2 - lambda1
  196. // Δφ = φ2 - φ1
  197. let deltaPhi = phi2 - phi1
  198. // sin²(Δφ/2)
  199. let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2)
  200. // sin²(Δλ/2)
  201. let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2)
  202. // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2))
  203. let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared)
  204. // This is the unit distance, i.e. assumes the circle has a radius of 1.
  205. let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC))
  206. // Scale it by the radius of the Earth in meters.
  207. let earthRadius = 6_371_000.0
  208. return unitDistance * earthRadius
  209. }
  210. private func radians(degreesInE7 degrees: Int32) -> Double {
  211. return (Double(degrees) / 1e7) * .pi / 180.0
  212. }