适用于C#、JavaScript和Python的简单示例中的gRPC

628 篇文章 16 订阅
262 篇文章 9 订阅

目录

介绍

gRPC的好处

Proto文件

文章大纲

代码位置

简单(回复/请求)gRPC示例

Protos项目

运行C#服务器和C#客户端

SimpleGrpcClient代码

SimpleGrpcServer代码

Node JS gRPC客户端

Python gRPC客户端

简单中继gRPC示例

中继示例Protos

运行服务器和客户端

C#发布客户端

C#订阅客户端

服务器代码

发布Node JS示例

订阅Node JS示例

发布Python示例

订阅Python示例

结论


介绍

gRPC的好处

Microsoft WCF几乎停止使用,因为它没有将其代码包含在.NET Core中。对于可能在不同计算机上运行的不同进程之间的远程通信,最好和最受欢迎的剩余解决方案是gRPCGoogle远程过程调用。

gRPC具有以下优点:

  1. GRPC适用于所有具有大多数软件语言的流行平台,包括但不限于
    1. C#
    2. Java
    3. JavaScript/TypeScript
    4. Python
    5. Go
    6. Objective-C
  2. gRPC非常快,占用的带宽很小——数据由Protobuffer软件以最合理的方式打包。
  3. gRPC及其底层Protobuffer非常易于学习且使用起来非常自然。
  4. 除了通常的请求-答复范例之外,gRPC还使用发布/订阅范例,其中订阅的客户端可以接收已发布消息的异步流。此类流可以很容易地更改为 Rx的IObservable,相应地可以应用所有允许的Rx LINQ转换。

Proto文件

gRPCProtobuffer使用简单的 .proto 文件来定义服务和消息。或者,在C#和其他语言中,可以使用代码优先方法来定义gRPC服务和来自属性语言代码的消息。

当想要在所有gRPC客户端和服务器中坚持使用相同的语言时,最好使用代码优先方法。我更感兴趣的是用不同语言编写的客户端可以访问用C#编写的同一服务器的情况。特别是,我对C#PythonJavaScript/TypeScript客户端感兴趣。因此,本文中的所有示例都将使用 .proto 文件来定义消息和服务。

文章大纲

本文介绍了两个示例——一个演示回复/请求,另一个演示发布/订阅范例。对于每个示例,服务器是用C#编写的,客户端是用三种不同的语言编写的:C#NodeJSJavaScript)和Python

代码位置

所有示例代码都位于 GRPC 文件夹下的NP.Samples中。以下所有文件夹引用都将与存储库的 GRPC 文件夹有关。

简单(回复/请求)gRPC示例

SimpleGrpcServer.sln解决方案位于 SimpleRequestReplySample\SimpleGrpcServer 文件夹下。该解决方案由五个项目组成:

  1. Protos——包含 service.proto gRPC protobuffer文件
  2. SimpleGrpcServer——C# Grpc服务器
  3. SimpleGrpcClient——C# Grpc客户端
  4. SimpleNodeJSGrpcClient——Node JS Grpc客户端
  5. SimplePythonGrpcClient——Python Grpc客户端

Protos项目

Protos.csproj 项目是一个.NET项目,将其 service.proto 文件编译为C#服务器和客户端项目的.NET代码。非C#项目仅使用其 service.proto 文件生成相应语言的客户端存根。

Protos.csproj 引用了三个nuget包——Google.ProtobufGrpcGrpc.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。请注意,这些消息与JavaC#非常相似,只是字段名称后跟数字——例如string name = 1;。对于同一消息中的每个字段,数字应该是唯一的,并将用于按数字顺序存储和恢复消息字段。

在我们的例子中,每条消息只有一个字段,因此,任何数字都可以(我们有数字1表示这是消息中的第一个(也是唯一一个)字段。

该服务Greeter包含一个调用SayHellorpc(远程过程调用),该调用将消息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.csProgram.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 Pythonstring打印到控制台窗口。

以下是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项目和三个文件夹组成:CSHARPNodeJS  Python。其中每个文件夹都将包含两个客户端——发布客户端和订阅客户端:

中继示例Protos

与前面的示例相同,service.proto 文件仅编译为.NET.C#项目——PythonNodeJS客户端使用自己的机制来解析文件。

// 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代码,用于启动服务器将其绑定到RelayServerRelayServiceImplementations 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#JavaScriptPython)编写的客户端。我为请求/回复和发布/订阅范例提供了示例。唯一遗漏的范例是当客户端将流作为输入发送到服务器时,但它们不太常见,我将来可能会添加一个涵盖它们的部分。

https://www.codeproject.com/Articles/5352930/gRPC-in-Easy-Samples-for-Csharp-JavaScript-and-Pyt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值