目录
介绍
gRPC的好处
Microsoft WCF几乎停止使用,因为它没有将其代码包含在.NET Core中。对于可能在不同计算机上运行的不同进程之间的远程通信,最好和最受欢迎的剩余解决方案是gRPC或Google远程过程调用。
gRPC具有以下优点:
- GRPC适用于所有具有大多数软件语言的流行平台,包括但不限于
- C#
- Java
- JavaScript/TypeScript
- Python
- Go
- Objective-C
- gRPC非常快,占用的带宽很小——数据由Protobuffer软件以最合理的方式打包。
- gRPC及其底层Protobuffer非常易于学习且使用起来非常自然。
- 除了通常的请求-答复范例之外,gRPC还使用发布/订阅范例,其中订阅的客户端可以接收已发布消息的异步流。此类流可以很容易地更改为 Rx的IObservable,相应地可以应用所有允许的Rx LINQ转换。
Proto文件
gRPC和Protobuffer使用简单的 .proto 文件来定义服务和消息。或者,在C#和其他语言中,可以使用代码优先方法来定义gRPC服务和来自属性语言代码的消息。
当想要在所有gRPC客户端和服务器中坚持使用相同的语言时,最好使用代码优先方法。我更感兴趣的是用不同语言编写的客户端可以访问用C#编写的同一服务器的情况。特别是,我对C#,Python和JavaScript/TypeScript客户端感兴趣。因此,本文中的所有示例都将使用 .proto 文件来定义消息和服务。
文章大纲
本文介绍了两个示例——一个演示回复/请求,另一个演示发布/订阅范例。对于每个示例,服务器是用C#编写的,客户端是用三种不同的语言编写的:C#、NodeJS(JavaScript)和Python。
代码位置
所有示例代码都位于 GRPC 文件夹下的NP.Samples中。以下所有文件夹引用都将与存储库的 GRPC 文件夹有关。
简单(回复/请求)gRPC示例
SimpleGrpcServer.sln解决方案位于 SimpleRequestReplySample\SimpleGrpcServer 文件夹下。该解决方案由五个项目组成:
- Protos——包含 service.proto gRPC protobuffer文件
- SimpleGrpcServer——C# Grpc服务器
- SimpleGrpcClient——C# Grpc客户端
- SimpleNodeJSGrpcClient——Node JS Grpc客户端
- SimplePythonGrpcClient——Python Grpc客户端
Protos项目
Protos.csproj 项目是一个.NET项目,将其 service.proto 文件编译为C#服务器和客户端项目的.NET代码。非C#项目仅使用其 service.proto 文件生成相应语言的客户端存根。
Protos.csproj 引用了三个nuget包——Google.Protobuf,Grpc和Grpc.Tools:
我提出的示例是Greeter Grpc服务的一个非常流行的示例,例如,可以在 .NET上的gRPC概述和 Grpc快速入门中找到。
客户端向服务器发送名称,例如“Joe Doe”,服务器回复“Hello Joe Doe”消息。
// taken from
// https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/simple_hello_world/
syntax = "proto3";
package greet;
service Greeter
{
// client takes HelloRequest and returns HelloReply
rpc SayHello (HelloRequest) returns (HelloReply);
}
// HelloRequest has only one string field - name
message HelloRequest
{
string name = 1;
}
// HelloReply has only one string name - msg
message HelloReply
{
string msg = 1;
}
proto文件基本上包含消息(请求和回复)和服务Greeter。请注意,这些消息与Java或C#非常相似,只是字段名称后跟数字——例如string name = 1;。对于同一消息中的每个字段,数字应该是唯一的,并将用于按数字顺序存储和恢复消息字段。
在我们的例子中,每条消息只有一个字段,因此,任何数字都可以(我们有数字1表示这是消息中的第一个(也是唯一一个)字段。
该服务Greeter包含一个调用SayHello的rpc(远程过程调用),该调用将消息HelloRequest作为其输入并返回HelloReply消息作为其输出。请注意,虽然RPC的接口由proto文件定义,但实现仍由服务器决定。
为了使Visual Studio自动生成.NET代码,创建客户端和服务器存根,service.proto文件的BuildAction应设置为“Protobuf Compiler”(一旦你引用Grpc.Tools,此选项将出现在生成操作中):
如上所述,存根将仅为C#客户端和服务器自动生成。其他语言将使用自己的方法直接从service.proto文件生成存根。
运行C#服务器和C#客户端
若要运行服务器,请在“解决方案资源管理器”中右键单击“SimpleGrpcServer ”项目,然后选择“调试->启动(不调试):
将打开一个空的命令提示符(因为服务器是控制台应用程序)。
现在,从同一解决方案运行C#客户端(通过右键单击“解决方案资源管理器”中的“SimpleGrpcClient”,然后选择“调试”->“运行而不调试”)。
客户端将显示从服务器返回的“Hello C# Client” string。
SimpleGrpcClient代码
所有C# Client代码都位于SimpleGrpcClient项目的program.cs文件中:
using Grpc.Core;
using static Greet.Greeter;
// get the channel connecting the client to the server
var channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
// create the GreeterClient service
var client = new GreeterClient(channel);
// call SetHello RPC on the server asynchronously and wait for the reply.
var reply =
await client.SayHelloAsync(new Greet.HelloRequest { Name = "C# Client" });
// print the Msg within the reply.
Console.WriteLine(reply.Msg);
SimpleGrpcServer代码
服务器代码包含在两个文件中——GreeterImplementation.cs和Program.cs。
GreeterImplementation——是一个类派生自abstract GreeterBase生成的服务器存根类。它提供了SayHello(...)方法的实现(其在超类GreeterBase中是abstract)。这里的实现以request.Name中包含的"Hello "string开头:
internal class GreeterImplementation : Greeter.GreeterBase
{
// provides implementation for the abstract method SayHello(...)
// from the generated server stub
public override async Task<HelloReply> SayHello
(
HelloRequest request,
ServerCallContext context)
{
// return HelloReply with Msg consisting of the word Hello
// and the name passed by the request
return new HelloReply
{
Msg = $"Hello {request.Name}"
};
}
}
这是program.cs代码:
using Greet;
using Grpc.Core;
using SimpleGrpcServerTest;
// create GreeterImplementation object providing the
// RPC SayHello implementation
GreeterImplementation greeterImplementation = new GreeterImplementation();
// bind the server with the greeterImplementation so that SayHello RPC called on
// the server will be channeled over to greeterImplementation.SayHello
Server server = new Server
{
Services = { Greeter.BindService(greeterImplementation) }
};
// set the server host, port and security (insecure)
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));
// start the server
server.Start();
// wait with shutdown until the user presses a key
Console.ReadLine();
// shutdown the server
server.ShutdownAsync().Wait();
最重要的行是绑定服务和GreeterImplementation:
// bind the server with the greeterImplementation so that SayHello RPC called on
// the server will be channeled over to greeterImplementation.SayHello
Server server = new Server
{
Services = { Greeter.BindService(greeterImplementation) }
};
Node JS gRPC客户端
若要运行客户端,请首先确保服务器已在运行。
然后重建SimpleNodeJSGrpcClient客户端以还原npm包(您只需要执行一次)。
最后,右键单击SimpleNodeJSGrpcClient项目并选择“调试->启动(不调试)”。
客户端控制台应打印从服务器返回的“Hello Java Script” string。
以下是Node JS客户端的代码(带注释):
module;
// import grpc functionality
let grpc = require('@grpc/grpc-js');
// import protoLoader functionality
let protoLoader = require('@grpc/proto-loader');
// load the services from service.proto file
const root =
protoLoader.loadSync
(
'../Protos/service.proto', // path to service.proto file
{
keepCase: true, // service loading parameters
longs: String,
enums: String,
defaults: true,
oneofs: true
});
// get the client package definitions for greet package
// defined within the services.proto file
const greet = grpc.loadPackageDefinition(root).greet;
// connect the client to the server
const client = new greet.Greeter("localhost:5555", grpc.credentials.createInsecure());
// call sayHello RPC passing "Java Script" as the name parameter
client.sayHello({ name: "Java Script" }, function (err, response) {
// obtain the response and print its msg field
console.log(response.msg);
});
// prevent the program from exiting right away
var done = (function wait() { if (!done) setTimeout(wait, 1000) })();
Python gRPC客户端
要准备Python gRCP客户端解决方案,请先右键单击SimplePythonGrpcClient项目下Python环境下的 env,然后选择“从requirements.txt安装”(只需要执行一次):
这将恢复所有必需的Python包。
然后,您可以像任何其他项目一样运行它,只需确保服务器已经在运行即可。
客户端 Python 项目应导致将“Hello Python” string打印到控制台窗口。
以下是Python代码(在Python注释中解释):
# import require packages
import grpc
import grpc_tools.protoc
# generate service_pb2 (for proto messages) and
# service_pb2_grpc (for RPCs) stubs
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../Protos/service.proto'
])
# import the generated stubs
import service_pb2;
import service_pb2_grpc;
# create the channel connecting to the server at localhost:5555
channel = grpc.insecure_channel('localhost:5555')
# get the server gRCP stub
greeterStub = service_pb2_grpc.GreeterStub(channel)
# call SayHello RPC on the server passing HelloRequest message
# whose name is set to 'Python'
response = greeterStub.SayHello(service_pb2.HelloRequest(name='Python'))
# print the result
print(response.msg)
简单中继gRPC示例
我们的下一个示例演示发布/订阅gRPC体系结构。简单中继服务器将客户端发布的消息传递给订阅它的每个客户端。
示例的解决方案StreamingRelayServer.sln位于 StreamingSample/StreamingRelayServer 文件夹下。
启动解决方案,您将看到它由服务器项目——StreamingRelayServer、包含protobuf service.proto 文件的Protos项目和三个文件夹组成:CSHARP、NodeJS 和 Python。其中每个文件夹都将包含两个客户端——发布客户端和订阅客户端:
中继示例Protos
与前面的示例相同,service.proto 文件仅编译为.NET的.C#项目——Python和NodeJS客户端使用自己的机制来解析文件。
// gRPC service (RelayService)
service RelayService
{
// Publish RPC - takes a Message with a msg string field
rpc Publish (Message) returns (PublishConfirmed) {}
// Subscribe RPC take SubscribeRequest and returns a stream
// of Message objects
rpc Subscribe(SubscribeRequest) returns (stream Message){}
}
// Relay Message class that
// contains a single msg field
message Message
{
string msg = 1;
}
// Empty class used to confirm that Published Message has been received
message PublishConfirmed
{
}
// Empty message that requests a subscription to Relay Messages.
message SubscribeRequest
{
}
请注意,rpc Subscribe返回的Messages流(不是单个Message)。
运行服务器和客户端
若要启动服务器,请右键单击StreamingRelayServer项目,然后选择“调试->启动(不调试)”。客户端应以完全相同的方式启动。为了观察正在发生某些事情,您需要先开始订阅客户端,然后才开始发布客户端。
例如,启动服务器,然后启动C# CSHARP/SubscribeSample。然后运行CSHARP/PublishSample。订阅客户端将在控制台窗口上打印“Published from C# Client”。
请记住,在第一次启动Node JS项目之前,必须构建它们(以便下载JavaScript包)。同样在第一次启动Python项目之前,您需要右键单击其Python环境->env并选择“从requirements.txt安装”以下载并安装Python包。
C#发布客户端
发布客户端代码包含在PublishSample项目的proram.cs文件中。下面是C#发布客户端的文档代码:
using Grpc.Core;
using Service;
using static Service.RelayService;
// Channel contains information for establishing a connection to the server
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
// create the RelayServiceClient from the channel
RelayServiceClient client = new RelayServiceClient(channel);
// call PublishAsync and get the confirmation reply
PublishConfirmed confirmation =
await client.PublishAsync(new Message { Msg = "Published from C# Client" });
C#订阅客户端
订阅客户端代码位于SubscribeSample项目的program.cs文件中。它从服务器获取replies流并从该流中打印消息:
// channel contains info for connecting to the server
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
// create RelayServiceClient
RelayServiceClient client = new RelayServiceClient(channel);
// replies is an async stream
using var replies = client.Subscribe(new Service.SubscribeRequest());
// move to the next message within the reply stream
while(await replies.ResponseStream.MoveNext())
{
// get the current message within reply stream
var message = replies.ResponseStream.Current;
// print the current message
Console.WriteLine(message.Msg);
}
replies流可能是无限的,并且仅在客户端或服务器终止连接时结束。如果没有来自服务器的新答复,则客户端将等待await replies.ResponseStream.MoveNext()。
服务器代码
一旦我们弄清楚了服务器的作用(通过演示其客户端),让我们来看看服务器在StreamingRelayServer项目中的代码。
下面是program.cs代码,用于启动服务器将其绑定到RelayServer的RelayServiceImplementations gRPC实现并将其连接到本地主机上的端口5555:
// bind RelayServiceImplementations to the gRPC server.
Server server = new Server
{
Services = { RelayService.BindService(new RelayServiceImplementations()) }
};
// set the server to be connected to port 5555 on the localhost without
// any security
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));
// start the server
server.Start();
// prevent the server from exiting.
Console.ReadLine();
RelayServiceImplementations类包含最有趣的代码,实现了从 service.proto 文件中定义的RelayService生成的gRPC存根的abstract方法的Publish(...)和Subscribe(...):
internal class RelayServiceImplementations : RelayServiceBase
{
// all client subscriptions
List<Subscription> _subscriptions = new List<Subscription>();
// Publish implementation
public override async Task<PublishConfirmed> Publish
(
Message request,
ServerCallContext context)
{
// add a published message to every subscription
foreach (Subscription subscription in _subscriptions)
{
subscription.AddMessage(request.Msg);
}
// return PublishConfirmed reply
return new PublishConfirmed();
}
// Subscribe implementation
public override async Task Subscribe
(
SubscribeRequest request,
IServerStreamWriter<Message> responseStream,
ServerCallContext context)
{
// create subscription object for a client subscription
Subscription subscription = new Subscription();
// add subscription to the list of subscriptions
_subscriptions.Add(subscription);
// subscription loop
while (true)
{
try
{
// take message one by one from subscription
string msg = subscription.TakeMessage(context.CancellationToken);
// create Message reply
Message message = new Message { Msg = msg };
// write the message into the output stream.
await responseStream.WriteAsync(message);
}
catch when(context.CancellationToken.IsCancellationRequested)
{
// if subscription is cancelled, break the loop
break;
}
}
// once the subscription is broken, remove it
// from the list of subscriptions
_subscriptions.Remove(subscription);
}
}
Publish(...)方法使用_subscriptions列表遍历每个订阅,并将新发布的消息添加到每个订阅中。
Subscribe(...)方法创建单个订阅并检查其中是否有新消息(按Publish(...)方法插入)。如果找到此类消息,则会将其删除并将其推送到响应流中。如果找不到此类消息,它将等待。
Subscribe连接断开后,将删除订阅。
下面是单个subscription对象的代码:
// represents a single client subscription
internal class Subscription
{
private BlockingCollection<string> _messages =
new BlockingCollection<string>();
// add a message to the _messages collection
public void AddMessage(string message)
{
_messages.Add(message);
}
// remove the first message from the _messages collection
// If there are no message in the collection, TakeMessage will wait
// blocking the thread.
public string TakeMessage(CancellationToken cancellationToken)
{
return _messages.Take(cancellationToken);
}
}
BlockingCollection将阻止订阅线程,直到其中有消息。由于每个订阅(或任何客户端操作)都在自己的线程中运行,因此其他订阅不会受到影响。
发布Node JS示例
PublishNodeJsSample项目——在其app.js文件中包含相关代码:
// import grpc packages
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');
// load and parse the service.proto file
const root = protoLoader.loadSync
(
'../../Protos/service.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
// get the service package containing RelayService object
const service = grpc.loadPackageDefinition(root).service;
// create the RelayService client connected to localhost:5555 port
const client = new service.RelayService
("localhost:5555", grpc.credentials.createInsecure());
// publish the Message object "Published from JS Client"
// (as long as the Json structure matches the Message object structure it will be
// converted to Message object)
client.Publish({ msg: "Published from JS Client" }, function (err, response) {
});
有趣的部分是client.Publish(...)代码。请注意,我们正在创建一个Json对象{ msg: "Published from JS Client" }作为该Publish(Message msg, ...)方法的输入。由于它的 Json 与 service.proto 文件中定义的Message对象的结构相匹配,因此此类对象将自动转换为服务器上的Message对象。
以下是service.proto Message的提醒:
// Relay Message class that
// contains a single msg field
message Message
{
string msg = 1;
}
订阅Node JS示例
此示例的重要代码位于SubscribeNodeJsSample项目的 app.js 文件中:
// import grpc packages
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');
// load and parse the service.proto file
const root = protoLoader.loadSync
(
'../../Protos/service.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
// get the service package containing RelayService object
const service = grpc.loadPackageDefinition(root).service;
// create the RelayService client connected to localhost:5555 port
const client = new service.RelayService
("localhost:5555", grpc.credentials.createInsecure());
// create the client subcription by passing
// an empty Json message (matching empty SubscribeRequest from service.proto)
var call = client.Subscribe({});
// process data stream combing from the server in
// response to calling Subscribe(...) gRPC
call.on('data', function (response) {
console.log(response.msg);
});
请注意,调用Subscribe(...) gRPC将返回一个JS委托,每次从服务器到达响应消息时都会调用该委托。
发布Python示例
此示例的代码位于PublishPythonSample项目的 PublishPythonSample.py 文件中:
# import python packages
import grpc
import grpc_tools.protoc
# generate the client stubs for service.proto file in python
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../../Protos/service.proto'
])
# import the client stubs (service_pb2 contains messages,
# service_pb2_grpc contains RPCs)
import service_pb2;
import service_pb2_grpc;
# create the channel
channel = grpc.insecure_channel('localhost:5555')
# create the client stub object for RelayService
stub = service_pb2_grpc.RelayServiceStub(channel);
# create and publish the message
response = stub.Publish(service_pb2.Message(msg='Publish from Python Client'));
重要说明:我们使用以下命令创建通道:
channel = grpc.insecure_channel('localhost:5555')
这意味着创建的通道和从中获取的stub将只允许阻塞对stub的调用——我们稍后使用的stub.Publish(...)调用是一个阻塞调用,它等待返回值,直到到服务器的往返完成。
如果要使用异步(非阻塞但可等待)调用,则应使用以下方法创建通道:
channel = grpc.aio.insecure_channel('localhost:5555')
请注意差异——.aio.上面的示例中缺少部分insecure_channel(...)方法路径。但是我们将在下面使用对insecure_channel(...)方法的异步版本的调用,其中我们给出了一个长期订阅连接的示例。
这看起来像是一个小细节,但我花了一些时间来弄清楚,所以希望这篇笔记能防止其他人遇到同样的错误。
订阅Python示例
以下是代码(来自同一命名项目的 SubscribePythonSample.py 文件):
# import python packages
import asyncio
import grpc
import grpc_tools.protoc
# generate the client stubs for service.proto file in python
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../../Protos/service.proto'
])
# import the client stubs (service_pb2 contains messages,
# service_pb2_grpc contains RPCs)
import service_pb2;
import service_pb2_grpc;
# define async loop
async def run() -> None:
#create the channel
async with grpc.aio.insecure_channel('localhost:5555') as channel:
# create the client stub object for RelayService
stub = service_pb2_grpc.RelayServiceStub(channel);
# call Subscribe gRCP and print the responses asynchronously
async for response in stub.Subscribe(service_pb2.SubscribeRequest()):
print(response.msg)
# run the async method calling that subscribes and
# prints the messages coming from the server
asyncio.run(run())
当然,请注意,要创建允许异步调用的通道,我们在grpc.aio. path中使用insecure_channel(...)方法的版本:
async with grpc.aio.insecure_channel('localhost:5555') as channel:
结论
在Microsoft几乎不推荐使用的WPC之后,gRPC——google RPC是创建各种服务器/客户端通信的最佳框架。除了通常的请求/回复范例之外,它还有助于实现具有输出和输入流的发布/订阅范例。
在本文中,我将演示如何使用gRPC服务器(用C#语言实现)和用不同语言(C#、JavaScript和Python)编写的客户端。我为请求/回复和发布/订阅范例提供了示例。唯一遗漏的范例是当客户端将流作为输入发送到服务器时,但它们不太常见,我将来可能会添加一个涵盖它们的部分。
https://www.codeproject.com/Articles/5352930/gRPC-in-Easy-Samples-for-Csharp-JavaScript-and-Pyt