Skip to content

Commit

Permalink
Add gRPC Web support.
Browse files Browse the repository at this point in the history
Initial implementation for supporting gRPC Web directly from
Swift gRPC services.
  • Loading branch information
sergiocampama committed Feb 22, 2019
1 parent 0195dfd commit 24c8e5d
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build
/protoc-gen-swiftgrpc
third_party/**
/Echo
/EchoNIO
/test.out
/echo.pid
/SwiftGRPC.xcodeproj
Expand Down
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ project-carthage:
@ruby fix-project-settings.rb SwiftGRPC-Carthage.xcodeproj || echo "xcodeproj ('sudo gem install xcodeproj') is required in order to generate the Carthage-compatible project!"
@ruby patch-carthage-project.rb SwiftGRPC-Carthage.xcodeproj || echo "xcodeproj ('sudo gem install xcodeproj') is required in order to generate the Carthage-compatible project!"

test: all
test: all
swift test $(CFLAGS)

test-echo: all
test-echo: all
cp .build/debug/Echo .
./Echo serve & /bin/echo $$! > echo.pid
./Echo get | tee test.out
Expand All @@ -40,6 +40,17 @@ test-echo: all
kill -9 `cat echo.pid`
diff -u test.out Sources/Examples/Echo/test.gold

test-echo-nio: all
cp .build/debug/EchoNIO .
cp .build/debug/Echo .
./EchoNIO serve & /bin/echo $$! > echo.pid
./Echo get | tee test.out
./Echo expand | tee -a test.out
./Echo collect | tee -a test.out
./Echo update | tee -a test.out
kill -9 `cat echo.pid`
diff -u test.out Sources/Examples/Echo/test.gold

test-plugin:
swift build $(CFLAGS) --product protoc-gen-swiftgrpc
protoc Sources/Examples/Echo/echo.proto --proto_path=Sources/Examples/Echo --plugin=.build/debug/protoc-gen-swift --plugin=.build/debug/protoc-gen-swiftgrpc --swiftgrpc_out=/tmp --swiftgrpc_opt=TestStubs=true
Expand Down
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var packageDependencies: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-nio-zlib-support.git", .upToNextMinor(from: "1.0.0")),
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMinor(from: "1.12.0")),
.package(url: "https://github.com/apple/swift-nio-nghttp2-support.git", .upToNextMinor(from: "1.0.0")),
.package(url: "https://github.com/apple/swift-nio-http2.git", .revision("dd9339e6310ad8537a271f3ff60a4f3976ca8e4d"))
.package(url: "https://github.com/apple/swift-nio-http2.git", .upToNextMinor(from: "0.2.1"))
]

var cGRPCDependencies: [Target.Dependency] = []
Expand Down Expand Up @@ -75,6 +75,12 @@ let package = Package(
"SwiftProtobuf",
"Commander"],
path: "Sources/Examples/Echo"),
.target(name: "EchoNIO",
dependencies: [
"SwiftGRPCNIO",
"SwiftProtobuf",
"Commander"],
path: "Sources/Examples/EchoNIO"),
.target(name: "Simple",
dependencies: ["SwiftGRPC", "Commander"],
path: "Sources/Examples/Simple"),
Expand Down
71 changes: 71 additions & 0 deletions Sources/Examples/EchoNIO/EchoProviderNIO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 Foundation
import NIO
import SwiftGRPCNIO

class EchoProviderNIO: Echo_EchoProvider_NIO {
func get(request: Echo_EchoRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Echo_EchoResponse> {
var response = Echo_EchoResponse()
response.text = "Swift echo get: " + request.text
return context.eventLoop.newSucceededFuture(result: response)
}

func expand(request: Echo_EchoRequest, context: StreamingResponseCallContext<Echo_EchoResponse>) -> EventLoopFuture<GRPCStatus> {
var endOfSendOperationQueue = context.eventLoop.newSucceededFuture(result: ())
let parts = request.text.components(separatedBy: " ")
for (i, part) in parts.enumerated() {
var response = Echo_EchoResponse()
response.text = "Swift echo expand (\(i)): \(part)"
endOfSendOperationQueue = endOfSendOperationQueue.then { context.sendResponse(response) }
}
return endOfSendOperationQueue.map { GRPCStatus.ok }
}

func collect(context: UnaryResponseCallContext<Echo_EchoResponse>) -> EventLoopFuture<(StreamEvent<Echo_EchoRequest>) -> Void> {
var parts: [String] = []
return context.eventLoop.newSucceededFuture(result: { event in
switch event {
case .message(let message):
parts.append(message.text)

case .end:
var response = Echo_EchoResponse()
response.text = "Swift echo collect: " + parts.joined(separator: " ")
context.responsePromise.succeed(result: response)
}
})
}

func update(context: StreamingResponseCallContext<Echo_EchoResponse>) -> EventLoopFuture<(StreamEvent<Echo_EchoRequest>) -> Void> {
var endOfSendOperationQueue = context.eventLoop.newSucceededFuture(result: ())
var count = 0
return context.eventLoop.newSucceededFuture(result: { event in
switch event {
case .message(let message):
var response = Echo_EchoResponse()
response.text = "Swift echo update (\(count)): \(message.text)"
endOfSendOperationQueue = endOfSendOperationQueue.then { context.sendResponse(response) }
count += 1

case .end:
endOfSendOperationQueue
.map { GRPCStatus.ok }
.cascade(promise: context.statusPromise)
}
})
}
}
1 change: 1 addition & 0 deletions Sources/Examples/EchoNIO/Generated/echo.pb.swift
1 change: 1 addition & 0 deletions Sources/Examples/EchoNIO/Generated/echo_nio.grpc.swift
11 changes: 11 additions & 0 deletions Sources/Examples/EchoNIO/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
all:
swift build -c debug --product EchoNIO
cp .build/debug/EchoNIO .

project:
swift package generate-xcodeproj

clean :
rm -rf Packages googleapis .build
rm -f Package.pins Echo google.json
rm -rf Package.resolved EchoNIO.xcodeproj EchoNIO
6 changes: 6 additions & 0 deletions Sources/Examples/EchoNIO/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# EchoNIO, a gRPC NIO Sample App

This directory contains a simple echo server that demonstrates
all four gRPC API styles (Unary, Server Streaming, Client
Streaming, and Bidirectional Streaming) using the NIO based
Swift gRPC implementation.
7 changes: 7 additions & 0 deletions Sources/Examples/EchoNIO/RUNME
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
#
# Use this to run the swift-proto generator
#
protoc echo.proto \
--swift_out=Generated \
--swiftgrpc_out=Client=false,Server=true,NIO=true:Generated
1 change: 1 addition & 0 deletions Sources/Examples/EchoNIO/echo.proto
51 changes: 51 additions & 0 deletions Sources/Examples/EchoNIO/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 Commander
import Dispatch
import Foundation
import NIO
import SwiftGRPCNIO

// Common flags and options
func addressOption(_ address: String) -> Option<String> {
return Option("address", default: address, description: "address of server")
}

let portOption = Option("port",
default: "8080",
description: "port of server")

Group {
$0.command("serve",
addressOption("0.0.0.0"),
portOption,
description: "Run an echo server.") { address, port in
let sem = DispatchSemaphore(value: 0)
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

print("starting insecure server")
_ = try! GRPCServer.start(hostname: address,
port: Int(port)!,
eventLoopGroup: eventLoopGroup,
serviceProviders: [EchoProviderNIO()])
.wait()

// This blocks to keep the main thread from finishing while the server runs,
// but the server never exits. Kill the process to stop it.
_ = sem.wait()
}

}.run()
12 changes: 8 additions & 4 deletions Sources/SwiftGRPCNIO/GRPCChannelHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public final class GRPCChannelHandler {
extension GRPCChannelHandler: ChannelInboundHandler {
public typealias InboundIn = RawGRPCServerRequestPart
public typealias OutboundOut = RawGRPCServerResponsePart

public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let requestPart = self.unwrapInboundIn(data)
switch requestPart {
Expand All @@ -61,9 +61,13 @@ extension GRPCChannelHandler: ChannelInboundHandler {
assert(handlerWasRemoved)

ctx.pipeline.add(handler: callHandler, after: codec).whenComplete {
var responseHeaders = HTTPHeaders()
responseHeaders.add(name: "content-type", value: "application/grpc")
ctx.write(self.wrapOutboundOut(.headers(responseHeaders)), promise: nil)
// Send the .headers event back to begin the headers flushing for the response.
// At this point, which headers should be returned is not known, as the content type is
// processed in HTTP1ToRawGRPCServerCodec. At the same time the HTTP1ToRawGRPCServerCodec
// handler doesn't have the data to determine whether headers should be returned, as it is
// this handler that checks whether the stub for the requested Service/Method is implemented.
// This likely signals that the architecture for these handlers could be improved.
ctx.write(self.wrapOutboundOut(.headers(HTTPHeaders())), promise: nil)
}
}

Expand Down
15 changes: 5 additions & 10 deletions Sources/SwiftGRPCNIO/GRPCServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,11 @@ public final class GRPCServer {

// Set the handlers that are applied to the accepted Channels
.childChannelInitializer { channel in
//! FIXME: Add an option for gRPC-via-HTTP1 (pPRC).
return channel.pipeline.add(handler: HTTP2Parser(mode: .server)).then {
let multiplexer = HTTP2StreamMultiplexer { (channel, streamID) -> EventLoopFuture<Void> in
return channel.pipeline.add(handler: HTTP2ToHTTP1ServerCodec(streamID: streamID))
.then { channel.pipeline.add(handler: HTTP1ToRawGRPCServerCodec()) }
.then { channel.pipeline.add(handler: GRPCChannelHandler(servicesByName: servicesByName)) }
}

return channel.pipeline.add(handler: multiplexer)
}
return channel.pipeline.add(handler: HTTPProtocolSwitcher {
channel -> EventLoopFuture<Void> in
return channel.pipeline.add(handler: HTTP1ToRawGRPCServerCodec())
.then { channel.pipeline.add(handler: GRPCChannelHandler(servicesByName: servicesByName)) }
})
}

// Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels
Expand Down
Loading

0 comments on commit 24c8e5d

Please sign in to comment.