Skip to content

Commit

Permalink
For service servers, replace inheritance with composition.
Browse files Browse the repository at this point in the history
This lets us spin up one server handling multiple gRPC services, where each service has its own `Provider` implementation. I.e. we can now serve multiple services from the same port.
  • Loading branch information
MrMage committed Jun 12, 2018
1 parent 8ce6ad0 commit 98f7fa6
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 136 deletions.
30 changes: 15 additions & 15 deletions Examples/EchoXcode/Echo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!

var echoProvider: Echo_EchoProvider!
var insecureServer: Echo_EchoServer!
var secureServer: Echo_EchoServer!
var insecureServer: ServiceServer!
var secureServer: ServiceServer!

func applicationDidFinishLaunching(_: Notification) {
// instantiate our custom-written application handler
echoProvider = EchoProvider()

// create and start a server for handling insecure requests
insecureServer = Echo_EchoServer(address: "localhost:8081",
provider: echoProvider)
insecureServer.start()

// create and start a server for handling secure requests
let certificateURL = Bundle.main.url(forResource: "ssl", withExtension: "crt")!
let keyURL = Bundle.main.url(forResource: "ssl", withExtension: "key")!
secureServer = Echo_EchoServer(address: "localhost:8443",
certificateURL: certificateURL,
keyURL: keyURL,
provider: echoProvider)
secureServer.start()
// create and start a server for handling insecure requests
insecureServer = ServiceServer(address: "localhost:8081",
services: [echoProvider])
insecureServer.start()
// create and start a server for handling secure requests
let certificateURL = Bundle.main.url(forResource: "ssl", withExtension: "crt")!
let keyURL = Bundle.main.url(forResource: "ssl", withExtension: "key")!
secureServer = ServiceServer(address: "localhost:8443",
certificateURL: certificateURL,
keyURL: keyURL,
services: [echoProvider])
secureServer.start()
}
}
86 changes: 34 additions & 52 deletions Sources/Examples/Echo/Generated/echo.grpc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,46 @@ class Echo_EchoServiceTestStub: ServiceClientTestStubBase, Echo_EchoService {
/// To build a server, implement a class that conforms to this protocol.
/// If one of the methods returning `ServerStatus?` returns nil,
/// it is expected that you have already returned a status to the client by means of `session.close`.
internal protocol Echo_EchoProvider {
internal protocol Echo_EchoProvider: ServiceProvider {
func get(request: Echo_EchoRequest, session: Echo_EchoGetSession) throws -> Echo_EchoResponse
func expand(request: Echo_EchoRequest, session: Echo_EchoExpandSession) throws -> ServerStatus?
func collect(session: Echo_EchoCollectSession) throws -> Echo_EchoResponse?
func update(session: Echo_EchoUpdateSession) throws -> ServerStatus?
}

extension Echo_EchoProvider {
internal var serviceName: String { return "echo.Echo" }

/// Determines and calls the appropriate request handler, depending on the request's method.
/// Throws `HandleMethodError.unknownMethod` for methods not handled by this service.
internal func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? {
switch method {
case "/echo.Echo/Get":
return try Echo_EchoGetSessionBase(
handler: handler,
providerBlock: { try self.get(request: $0, session: $1 as! Echo_EchoGetSessionBase) })
.run()
case "/echo.Echo/Expand":
return try Echo_EchoExpandSessionBase(
handler: handler,
providerBlock: { try self.expand(request: $0, session: $1 as! Echo_EchoExpandSessionBase) })
.run()
case "/echo.Echo/Collect":
return try Echo_EchoCollectSessionBase(
handler: handler,
providerBlock: { try self.collect(session: $0 as! Echo_EchoCollectSessionBase) })
.run()
case "/echo.Echo/Update":
return try Echo_EchoUpdateSessionBase(
handler: handler,
providerBlock: { try self.update(session: $0 as! Echo_EchoUpdateSessionBase) })
.run()
default:
throw HandleMethodError.unknownMethod
}
}
}

internal protocol Echo_EchoGetSession: ServerSessionUnary {}

fileprivate final class Echo_EchoGetSessionBase: ServerSessionUnaryBase<Echo_EchoRequest, Echo_EchoResponse>, Echo_EchoGetSession {}
Expand Down Expand Up @@ -303,54 +336,3 @@ fileprivate final class Echo_EchoUpdateSessionBase: ServerSessionBidirectionalSt

class Echo_EchoUpdateSessionTestStub: ServerSessionBidirectionalStreamingTestStub<Echo_EchoRequest, Echo_EchoResponse>, Echo_EchoUpdateSession {}


/// Main server for generated service
internal final class Echo_EchoServer: ServiceServer {
private let provider: Echo_EchoProvider

internal init(address: String, provider: Echo_EchoProvider) {
self.provider = provider
super.init(address: address)
}

internal init?(address: String, certificateURL: URL, keyURL: URL, provider: Echo_EchoProvider) {
self.provider = provider
super.init(address: address, certificateURL: certificateURL, keyURL: keyURL)
}

internal init?(address: String, certificateString: String, keyString: String, provider: Echo_EchoProvider) {
self.provider = provider
super.init(address: address, certificateString: certificateString, keyString: keyString)
}

/// Determines and calls the appropriate request handler, depending on the request's method.
/// Throws `HandleMethodError.unknownMethod` for methods not handled by this service.
internal override func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? {
let provider = self.provider
switch method {
case "/echo.Echo/Get":
return try Echo_EchoGetSessionBase(
handler: handler,
providerBlock: { try provider.get(request: $0, session: $1 as! Echo_EchoGetSessionBase) })
.run()
case "/echo.Echo/Expand":
return try Echo_EchoExpandSessionBase(
handler: handler,
providerBlock: { try provider.expand(request: $0, session: $1 as! Echo_EchoExpandSessionBase) })
.run()
case "/echo.Echo/Collect":
return try Echo_EchoCollectSessionBase(
handler: handler,
providerBlock: { try provider.collect(session: $0 as! Echo_EchoCollectSessionBase) })
.run()
case "/echo.Echo/Update":
return try Echo_EchoUpdateSessionBase(
handler: handler,
providerBlock: { try provider.update(session: $0 as! Echo_EchoUpdateSessionBase) })
.run()
default:
throw HandleMethodError.unknownMethod
}
}
}

14 changes: 6 additions & 8 deletions Sources/Examples/Echo/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,19 @@ Group {
portOption,
description: "Run an echo server.") { ssl, address, port in
let sem = DispatchSemaphore(value: 0)
let echoProvider = EchoProvider()
var echoServer: Echo_EchoServer?
var echoServer: ServiceServer?
if ssl {
print("starting secure server")
let certificateURL = URL(fileURLWithPath: "ssl.crt")
let keyURL = URL(fileURLWithPath: "ssl.key")
echoServer = Echo_EchoServer(address: address + ":" + port,
certificateURL: certificateURL,
keyURL: keyURL,
provider: echoProvider)
echoServer = ServiceServer(address: address + ":" + port,
certificateURL: certificateURL,
keyURL: keyURL,
services: [EchoProvider()])
echoServer?.start()
} else {
print("starting insecure server")
echoServer = Echo_EchoServer(address: address + ":" + port,
provider: echoProvider)
echoServer = ServiceServer(address: address + ":" + port, services: [EchoProvider()])
echoServer?.start()
}
// This blocks to keep the main thread from finishing while the server runs,
Expand Down
33 changes: 33 additions & 0 deletions Sources/SwiftGRPC/Runtime/ServiceProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2018, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Dispatch
import Foundation
import SwiftProtobuf

public enum HandleMethodError: Error {
case unknownMethod
}

// Helper protocol to let one `ServiceServer` serve multiple gRPC services.
// Implementations for these methods are usually provided by the code generated by our protoc plugin.
public protocol ServiceProvider {
// The name of the service (including package names) provided by this object, e.g. "echo.Echo".
var serviceName: String { get }

/// Handle the given method. Needs to be overridden by actual implementations.
func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus?
}
29 changes: 16 additions & 13 deletions Sources/SwiftGRPC/Runtime/ServiceServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,36 @@ open class ServiceServer {
public let server: Server

public var shouldLogRequests = true


fileprivate let servicesByName: [String: ServiceProvider]

/// Create a server that accepts insecure connections.
public init(address: String) {
public init(address: String, services: [ServiceProvider]) {
gRPC.initialize()
self.address = address
server = Server(address: address)
servicesByName = Dictionary(uniqueKeysWithValues: services.map { ($0.serviceName, $0) })
}

/// Create a server that accepts secure connections.
public init(address: String, certificateString: String, keyString: String) {
public init(address: String, certificateString: String, keyString: String, services: [ServiceProvider]) {
gRPC.initialize()
self.address = address
server = Server(address: address, key: keyString, certs: certificateString)
servicesByName = Dictionary(uniqueKeysWithValues: services.map { ($0.serviceName, $0) })
}

/// Create a server that accepts secure connections.
public init?(address: String, certificateURL: URL, keyURL: URL) {
public init?(address: String, certificateURL: URL, keyURL: URL, services: [ServiceProvider]) {
guard let certificate = try? String(contentsOf: certificateURL, encoding: .utf8),
let key = try? String(contentsOf: keyURL, encoding: .utf8)
else { return nil }
gRPC.initialize()
self.address = address
server = Server(address: address, key: key, certs: certificate)
servicesByName = Dictionary(uniqueKeysWithValues: services.map { ($0.serviceName, $0) })
}

public enum HandleMethodError: Error {
case unknownMethod
}

/// Handle the given method. Needs to be overridden by actual implementations.
/// Returns whether the method was actually handled.
open func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? { fatalError("needs to be overridden") }

/// Start the server.
public func start() {
server.run { [weak self] handler in
Expand All @@ -76,7 +73,13 @@ open class ServiceServer {

do {
do {
if let responseStatus = try strongSelf.handleMethod(unwrappedMethod, handler: handler),
let methodComponents = unwrappedMethod.components(separatedBy: "/")
guard methodComponents.count >= 3 && methodComponents[0].isEmpty,
let providerForServiceName = strongSelf.servicesByName[methodComponents[1]] else {
throw HandleMethodError.unknownMethod
}

if let responseStatus = try providerForServiceName.handleMethod(unwrappedMethod, handler: handler),
!handler.completionQueue.hasBeenShutdown {
// The handler wants us to send the status for them; do that.
// But first, ensure that all outgoing messages have been enqueued, to avoid ending the stream prematurely:
Expand Down
16 changes: 8 additions & 8 deletions Sources/protoc-gen-swiftgrpc/Generator-Names.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ extension Generator {
return nameForPackageService(file, service) + "Provider"
}

internal var serverName: String {
return nameForPackageService(file, service) + "Server"
}

internal var callName: String {
return nameForPackageServiceMethod(file, service, method) + "Call"
}
Expand All @@ -75,12 +71,16 @@ extension Generator {
internal var methodOutputName: String {
return protoMessageName(method.outputType)
}

internal var methodPath: String {
internal var servicePath: String {
if !file.package.isEmpty {
return "\"/" + file.package + "." + service.name + "/" + method.name + "\""
return file.package + "." + service.name
} else {
return "\"/" + service.name + "/" + method.name + "\""
return service.name
}
}

internal var methodPath: String {
return "\"/" + servicePath + "/" + method.name + "\""
}
}
40 changes: 6 additions & 34 deletions Sources/protoc-gen-swiftgrpc/Generator-Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,13 @@ extension Generator {
}
println()
}
println()
printServerMainClass()
}

private func printServerProtocol() {
println("/// To build a server, implement a class that conforms to this protocol.")
println("/// If one of the methods returning `ServerStatus?` returns nil,")
println("/// it is expected that you have already returned a status to the client by means of `session.close`.")
println("\(access) protocol \(providerName) {")
println("\(access) protocol \(providerName): ServiceProvider {")
indent()
for method in service.methods {
self.method = method
Expand All @@ -61,40 +59,14 @@ extension Generator {
outdent()
println("}")
println()
}

private func printServerMainClass() {
println("/// Main server for generated service")
println("\(access) final class \(serverName): ServiceServer {")
indent()
println("private let provider: \(providerName)")
println()
println("\(access) init(address: String, provider: \(providerName)) {")
println("extension \(providerName) {")
indent()
println("self.provider = provider")
println("super.init(address: address)")
outdent()
println("}")
println()
println("\(access) init?(address: String, certificateURL: URL, keyURL: URL, provider: \(providerName)) {")
indent()
println("self.provider = provider")
println("super.init(address: address, certificateURL: certificateURL, keyURL: keyURL)")
outdent()
println("}")
println()
println("\(access) init?(address: String, certificateString: String, keyString: String, provider: \(providerName)) {")
indent()
println("self.provider = provider")
println("super.init(address: address, certificateString: certificateString, keyString: keyString)")
outdent()
println("}")
println("\(access) var serviceName: String { return \"\(servicePath)\" }")
println()
println("/// Determines and calls the appropriate request handler, depending on the request's method.")
println("/// Throws `HandleMethodError.unknownMethod` for methods not handled by this service.")
println("\(access) override func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? {")
println("\(access) func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? {")
indent()
println("let provider = self.provider")
println("switch method {")
for method in service.methods {
self.method = method
Expand All @@ -105,7 +77,7 @@ extension Generator {
println("return try \(methodSessionName)Base(")
indent()
println("handler: handler,")
println("providerBlock: { try provider.\(methodFunctionName)(request: $0, session: $1 as! \(methodSessionName)Base) })")
println("providerBlock: { try self.\(methodFunctionName)(request: $0, session: $1 as! \(methodSessionName)Base) })")
indent()
println(".run()")
outdent()
Expand All @@ -114,7 +86,7 @@ extension Generator {
println("return try \(methodSessionName)Base(")
indent()
println("handler: handler,")
println("providerBlock: { try provider.\(methodFunctionName)(session: $0 as! \(methodSessionName)Base) })")
println("providerBlock: { try self.\(methodFunctionName)(session: $0 as! \(methodSessionName)Base) })")
indent()
println(".run()")
outdent()
Expand Down
Loading

0 comments on commit 98f7fa6

Please sign in to comment.