Skip to content

Commit 4ae7044

Browse files
committed
Add reflection server example
Motivation: We've added back the reflection server; we should have an example of how to use it. Modifications: Add a reflection server example Result: More examples
1 parent dee1f1a commit 4ae7044

File tree

8 files changed

+257
-38
lines changed

8 files changed

+257
-38
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
17+
import GRPCCore
18+
19+
struct EchoService: Echo_Echo.SimpleServiceProtocol {
20+
func get(
21+
request: Echo_EchoRequest,
22+
context: ServerContext
23+
) async throws -> Echo_EchoResponse {
24+
return .with { $0.text = request.text }
25+
}
26+
27+
func collect(
28+
request: RPCAsyncSequence<Echo_EchoRequest, any Error>,
29+
context: ServerContext
30+
) async throws -> Echo_EchoResponse {
31+
let messages = try await request.reduce(into: []) { $0.append($1.text) }
32+
let joined = messages.joined(separator: " ")
33+
return .with { $0.text = joined }
34+
}
35+
36+
func expand(
37+
request: Echo_EchoRequest,
38+
response: RPCWriter<Echo_EchoResponse>,
39+
context: ServerContext
40+
) async throws {
41+
let parts = request.text.split(separator: " ")
42+
let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } }
43+
try await response.write(contentsOf: messages)
44+
}
45+
46+
func update(
47+
request: RPCAsyncSequence<Echo_EchoRequest, any Error>,
48+
response: RPCWriter<Echo_EchoResponse>,
49+
context: ServerContext
50+
) async throws {
51+
for try await message in request {
52+
try await response.write(.with { $0.text = message.text })
53+
}
54+
}
55+
}

Examples/echo/Sources/Subcommands/Serve.swift

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,41 +41,3 @@ struct Serve: AsyncParsableCommand {
4141
}
4242
}
4343
}
44-
45-
struct EchoService: Echo_Echo.SimpleServiceProtocol {
46-
func get(
47-
request: Echo_EchoRequest,
48-
context: ServerContext
49-
) async throws -> Echo_EchoResponse {
50-
return .with { $0.text = request.text }
51-
}
52-
53-
func collect(
54-
request: RPCAsyncSequence<Echo_EchoRequest, any Error>,
55-
context: ServerContext
56-
) async throws -> Echo_EchoResponse {
57-
let messages = try await request.reduce(into: []) { $0.append($1.text) }
58-
let joined = messages.joined(separator: " ")
59-
return .with { $0.text = joined }
60-
}
61-
62-
func expand(
63-
request: Echo_EchoRequest,
64-
response: RPCWriter<Echo_EchoResponse>,
65-
context: ServerContext
66-
) async throws {
67-
let parts = request.text.split(separator: " ")
68-
let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } }
69-
try await response.write(contentsOf: messages)
70-
}
71-
72-
func update(
73-
request: RPCAsyncSequence<Echo_EchoRequest, any Error>,
74-
response: RPCWriter<Echo_EchoResponse>,
75-
context: ServerContext
76-
) async throws {
77-
for try await message in request {
78-
try await response.write(.with { $0.text = message.text })
79-
}
80-
}
81-
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// swift-tools-version:6.0
2+
/*
3+
* Copyright 2024, gRPC Authors All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import PackageDescription
19+
20+
let package = Package(
21+
name: "reflection-server",
22+
platforms: [.macOS(.v15)],
23+
dependencies: [
24+
.package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"),
25+
.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"),
26+
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"),
27+
.package(url: "https://github.com/grpc/grpc-swift-extras.git", branch: "main"),
28+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
29+
],
30+
targets: [
31+
.executableTarget(
32+
name: "reflection-server",
33+
dependencies: [
34+
.product(name: "GRPCCore", package: "grpc-swift"),
35+
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
36+
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
37+
.product(name: "GRPCReflectionService", package: "grpc-swift-extras"),
38+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
39+
],
40+
resources: [
41+
.copy("DescriptorSets")
42+
]
43+
)
44+
]
45+
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Reflection Server
2+
3+
This example demonstrates the gRPC Reflection service which is described in more
4+
detail in the [gRPC documentation](https://github.com/grpc/grpc/blob/6fa8043bf9befb070b846993b59a3348248e6566/doc/server-reflection.md).
5+
6+
## Overview
7+
8+
A "reflection-server" command line tool that uses the reflection service implementation
9+
from [grpc/grpc-swift-extras](https://github.com/grpc/grpc-swift-extras) and the
10+
Echo service (see the 'echo' example).
11+
12+
The reflection service requires you to initialize it with a set of Protobuf file
13+
descriptors for the services you're offering. You can use `protoc` to create a
14+
descriptor set including dependencies and source information for each service.
15+
16+
The following command will generate a descriptor set at `path/to/output.pb` from
17+
the `path/to/input.proto` file with source information and any imports used in
18+
`input.proto`:
19+
20+
```console
21+
protoc --descriptor_set_out=path/to/output.pb path/to/input.proto \
22+
--include_source_info \
23+
--include_imports
24+
```
25+
26+
## Usage
27+
28+
Build and run the server using the CLI:
29+
30+
```console
31+
$ swift run reflection-server
32+
Reflection server listening on [ipv4]127.0.0.1:31415
33+
```
34+
35+
You can use `grpcurl` to query the reflection service. If you don't already have
36+
it installed follow the instructions in the `grpcurl` project's
37+
[README](https://github.com/fullstorydev/grpcurl).
38+
39+
You can list all services with:
40+
41+
```console
42+
$ grpcurl -plaintext 127.0.0.1:31415 list
43+
echo.Echo
44+
```
45+
46+
And describe the 'Get' method in the 'echo.Echo' service:
47+
48+
```console
49+
$ grpcurl -plaintext 127.0.0.1:31415 describe echo.Echo.Get
50+
echo.Echo.Get is a method:
51+
// Immediately returns an echo of a request.
52+
rpc Get ( .echo.EchoRequest ) returns ( .echo.EchoResponse );
53+
```
54+
55+
You can also call the 'echo.Echo.Get' method:
56+
```console
57+
$ grpcurl -plaintext -d '{ "text": "Hello" }' 127.0.0.1:31415 echo.Echo.Get
58+
{
59+
"text": "Hello"
60+
}
61+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../echo/Sources/Subcommands/EchoService.swift
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
17+
import ArgumentParser
18+
import Foundation
19+
import GRPCCore
20+
import GRPCNIOTransportHTTP2
21+
import GRPCProtobuf
22+
import GRPCReflectionService
23+
24+
@main
25+
struct ReflectionServer: AsyncParsableCommand {
26+
@Option(help: "The port to listen on")
27+
var port: Int = 31415
28+
29+
func run() async throws {
30+
// Find descriptor sets ('*.pb') bundled with this example.
31+
let urls = Bundle.module.urls(
32+
forResourcesWithExtension: "pb",
33+
subdirectory: "DescriptorSets"
34+
)
35+
36+
// Start the server with the reflection service and the echo service.
37+
let server = GRPCServer(
38+
transport: .http2NIOPosix(
39+
address: .ipv4(host: "127.0.0.1", port: self.port),
40+
transportSecurity: .plaintext
41+
),
42+
services: [
43+
try ReflectionService(descriptorSetFileURLs: urls ?? []),
44+
EchoService(),
45+
]
46+
)
47+
48+
try await withThrowingDiscardingTaskGroup { group in
49+
group.addTask { try await server.serve() }
50+
if let address = try await server.listeningAddress?.ipv4 {
51+
print("Reflection server listening on \(address)")
52+
print(String(repeating: "-", count: 80))
53+
54+
let example = """
55+
If you have grpcurl installed you can query the service to discover services
56+
and make calls against them. You can install grpcurl by following the
57+
instruction in its repository: https://github.com/fullstorydev/grpcurl
58+
59+
Here are some example commands:
60+
61+
List all services:
62+
$ grpcurl -plaintext \(address.host):\(address.port) list
63+
64+
Describe the 'Get' method in the 'echo.Echo' service:
65+
$ grpcurl -plaintext \(address.host):\(address.port) describe echo.Echo.Get
66+
67+
Call the 'echo.Echo.Get' method:
68+
$ grpcurl -plaintext -d '{ "text": "Hello" }' \(address.host):\(address.port) echo.Echo.Get
69+
"""
70+
print(example)
71+
}
72+
}
73+
}
74+
}

dev/protos/generate.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ function generate_error_details_example {
105105
generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal"
106106
}
107107

108+
function generate_reflection_server_example {
109+
local proto="$here/examples/echo/echo.proto"
110+
local output="$root/Examples/reflection-server/Sources/Generated"
111+
local pb_output="$root/Examples/reflection-server/Sources/DescriptorSets/echo.pb"
112+
113+
generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal"
114+
generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal"
115+
invoke_protoc --descriptor_set_out="$pb_output" "$proto" -I "$(dirname "$proto")" \
116+
--include_source_info \
117+
--include_imports
118+
}
119+
108120
#- TESTS ----------------------------------------------------------------------
109121

110122
function generate_service_config_for_tests {
@@ -128,6 +140,7 @@ generate_echo_example
128140
generate_helloworld_example
129141
generate_routeguide_example
130142
generate_error_details_example
143+
generate_reflection_server_example
131144

132145
# Tests
133146
generate_service_config_for_tests

0 commit comments

Comments
 (0)