Serve.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 GRPCCore
  19. import GRPCNIOTransportHTTP2
  20. import GRPCProtobuf
  21. import Synchronization
  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. struct RouteGuideService {
  48. /// Known features.
  49. private let features: [Routeguide_Feature]
  50. /// Notes recorded by clients.
  51. private let receivedNotes: Notes
  52. /// A thread-safe store for notes sent by clients.
  53. private final class Notes: Sendable {
  54. private let notes: Mutex<[Routeguide_RouteNote]>
  55. init() {
  56. self.notes = Mutex([])
  57. }
  58. /// Records a note and returns all other notes recorded at the same location.
  59. ///
  60. /// - Parameter receivedNote: A note to record.
  61. /// - Returns: Other notes recorded at the same location.
  62. func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] {
  63. return self.notes.withLock { notes in
  64. var notesFromSameLocation: [Routeguide_RouteNote] = []
  65. for note in notes {
  66. if note.location == receivedNote.location {
  67. notesFromSameLocation.append(note)
  68. }
  69. }
  70. notes.append(receivedNote)
  71. return notesFromSameLocation
  72. }
  73. }
  74. }
  75. /// Creates a new route guide service.
  76. /// - Parameter features: Known features.
  77. init(features: [Routeguide_Feature]) {
  78. self.features = features
  79. self.receivedNotes = Notes()
  80. }
  81. /// Returns the first feature found at the given location, if one exists.
  82. private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? {
  83. self.features.first {
  84. $0.location.latitude == latitude && $0.location.longitude == longitude
  85. }
  86. }
  87. }
  88. extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol {
  89. func getFeature(
  90. request: ServerRequest<Routeguide_Point>,
  91. context: ServerContext
  92. ) async throws -> ServerResponse<Routeguide_Feature> {
  93. let feature = self.findFeature(
  94. latitude: request.message.latitude,
  95. longitude: request.message.longitude
  96. )
  97. if let feature {
  98. return ServerResponse(message: feature)
  99. } else {
  100. // No feature: return a feature with an empty name.
  101. let unknownFeature = Routeguide_Feature.with {
  102. $0.name = ""
  103. $0.location = .with {
  104. $0.latitude = request.message.latitude
  105. $0.longitude = request.message.longitude
  106. }
  107. }
  108. return ServerResponse(message: unknownFeature)
  109. }
  110. }
  111. func listFeatures(
  112. request: ServerRequest<Routeguide_Rectangle>,
  113. context: ServerContext
  114. ) async throws -> StreamingServerResponse<Routeguide_Feature> {
  115. return StreamingServerResponse { 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: StreamingServerRequest<Routeguide_Point>,
  125. context: ServerContext
  126. ) async throws -> ServerResponse<Routeguide_RouteSummary> {
  127. let startTime = ContinuousClock.now
  128. var pointsVisited = 0
  129. var featuresVisited = 0
  130. var distanceTravelled = 0.0
  131. var previousPoint: Routeguide_Point? = nil
  132. for try await point in request.messages {
  133. pointsVisited += 1
  134. if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil {
  135. featuresVisited += 1
  136. }
  137. if let previousPoint {
  138. distanceTravelled += greatCircleDistance(from: previousPoint, to: point)
  139. }
  140. previousPoint = point
  141. }
  142. let duration = startTime.duration(to: .now)
  143. let summary = Routeguide_RouteSummary.with {
  144. $0.pointCount = Int32(pointsVisited)
  145. $0.featureCount = Int32(featuresVisited)
  146. $0.elapsedTime = Int32(duration.components.seconds)
  147. $0.distance = Int32(distanceTravelled)
  148. }
  149. return ServerResponse(message: summary)
  150. }
  151. func routeChat(
  152. request: StreamingServerRequest<Routeguide_RouteNote>,
  153. context: ServerContext
  154. ) async throws -> StreamingServerResponse<Routeguide_RouteNote> {
  155. return StreamingServerResponse { writer in
  156. for try await note in request.messages {
  157. let notes = self.receivedNotes.recordNote(note)
  158. try await writer.write(contentsOf: notes)
  159. }
  160. return [:]
  161. }
  162. }
  163. }
  164. extension Routeguide_Feature {
  165. func isContained(
  166. by rectangle: Routeguide_Rectangle
  167. ) -> Bool {
  168. return rectangle.lo.latitude <= self.location.latitude
  169. && self.location.latitude <= rectangle.hi.latitude
  170. && rectangle.lo.longitude <= self.location.longitude
  171. && self.location.longitude <= rectangle.hi.longitude
  172. }
  173. }
  174. private func greatCircleDistance(
  175. from point1: Routeguide_Point,
  176. to point2: Routeguide_Point
  177. ) -> Double {
  178. // See https://en.wikipedia.org/wiki/Great-circle_distance
  179. //
  180. // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1.
  181. // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2.
  182. //
  183. // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1.
  184. //
  185. // The central angle between them, σc (sigmaC) can be computed as:
  186. //
  187. // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2))
  188. //
  189. // The unit distance (d) between point1 and point2 can then be computed as:
  190. //
  191. // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc))
  192. let lambda1 = radians(degreesInE7: point1.longitude)
  193. let phi1 = radians(degreesInE7: point1.latitude)
  194. let lambda2 = radians(degreesInE7: point2.longitude)
  195. let phi2 = radians(degreesInE7: point2.latitude)
  196. // Δλ = λ2 - λ1
  197. let deltaLambda = lambda2 - lambda1
  198. // Δφ = φ2 - φ1
  199. let deltaPhi = phi2 - phi1
  200. // sin²(Δφ/2)
  201. let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2)
  202. // sin²(Δλ/2)
  203. let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2)
  204. // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2))
  205. let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared)
  206. // This is the unit distance, i.e. assumes the circle has a radius of 1.
  207. let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC))
  208. // Scale it by the radius of the Earth in meters.
  209. let earthRadius = 6_371_000.0
  210. return unitDistance * earthRadius
  211. }
  212. private func radians(degreesInE7 degrees: Int32) -> Double {
  213. return (Double(degrees) / 1e7) * .pi / 180.0
  214. }