Serve.swift 7.9 KB

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