.NetCore3.1 gRPC 实战

2022-12-07 17:38:49 浏览数 (1)

一.概要

gRPC(Remote procedure call)是google开源的网络通讯框架;同时也是Cloud Native Computation基金会下的产品。本文章的项目源码会在结尾的联系方式中找到。

(1)gRPC这个框架可以为我们做什么?

在 gRPC 中,客户端应用程序可以直接将方法调用到其他计算机上的服务器应用程序上,就像它是本地对象一样,从而更轻松地创建分布式应用程序和服务。与许多 RPC 系统一样,gRPC 基于定义服务的想法,指定可以使用其参数和返回类型远程调用的方法。在服务器端,服务器实现此接口并运行 gRPC 服务器来处理客户端调用。在客户端,客户端具有一个存根(在某些语言中仅称为客户端),该存根提供与服务器相同的方法。

(2)gRPC优缺点

优点: protobuf二进制消息,性能好/效率高(空间和时间效率都很不错) proto文件生成目标代码,简单易用 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式) 支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级 支持多种语言(可以把proto文件看做IDL文件) Netty等一些框架集成

缺点: GRPC尚未提供连接池,需要自行实现 尚未提供“服务发现”、“负载均衡”机制 因为基于HTTP2,绝大多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持) Protobuf二进制可读性差(貌似提供了Text_Fromat功能) 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)

(3)多语言支持

支持多种语言(C, C , Python, PHP, Nodejs, C#, Objective-C、Golang、Java),并能够基于语言自动生成客户端和服务端功能库。

(4)使用protocol buffers

gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。下面是教你如何定义protobuf的教程。

zhuanlan.zhihu.com/p/19

(5)使用场景

  • 需要对接口进行严格约束的情况,不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素。这时gRPC就可以通过protobuf来提供严格的接口约束。
  • 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。

官网文档地址:

grpc.io/ grpc.io/docs/languages/

源码地址:

github.com/grpc/grpc-do

相关工具:

releases.llvm.org/downl github.com/protocolbuff protocol编译器 slproweb.com/products/W SSL证书生成

如果懒得下载文章结尾的联系方式当中可提供。

二.内容简介

  1. gRPC 生命周期
  2. gRPC 通讯原理
  3. gRPC 框架介绍
  4. gRPC Demo编写(服务端、客户端)

三.主要内容

RPC生命周期

现在让我们更仔细地看看当gRPC客户端调用gRPC服务器方法时会发生什么。我们不看实现细节,您可以在我们的语言特定页面中找到更多关于这些细节的信息。

一元RPC 首先,让我们看一下最简单的RPC类型,其中客户端发送单个请求服务返回单个响应。

一旦客户端调用stub/client对象上的方法,服务器就会被通知RPC已经被调用,调用时带有客户端的metadata、方法名称以及指定的截止日期(如果可用)。

然后,服务器可以立即返回自己的初始metadata(必须在任何响应之前发送),或者等待客户端的请求消息-首先发生的消息是特定于应用程序的。

一旦服务器收到客户端的请求消息,它就会做必要的工作来创建和填充其响应。然后,响应连同状态详细信息(状态代码和可选的状态消息)和可选的尾随metadata一起返回给客户端(如果成功)。

如果状态为OK,客户端会得到响应,从而在客户端完成调用。

服务器流式RPC 服务器流式RPC类似于上面的一元RPC,只是服务器在收到客户端的请求消息后会返回一个响应流。返回所有响应后,服务器的状态详细信息(状态代码和可选状态消息)和可选的尾随metadata将被发回服务器端完成。一旦客户端收到服务器的所有响应,它就会完成全部调用。

客户端流式RPC 客户端流式RPC也类似于一元RPC,只是客户端向服务器发送请求流,而不是单个请求。服务器发送回一个响应,通常但不一定是在收到所有客户端请求后,连同其状态详细信息和可选的尾随metadata。

双向流式RPC 在双向流式RPC中,调用再次由调用方法的客户端发起,服务器接收客户端metadata,、方法名称和截止日期。同样,服务器可以选择发回其初始metadata,,或者等待客户端开始发送请求。

接下来会发生什么取决于应用程序,因为客户端和服务器可以按任何顺序读写-这些流完全独立运行。例如,服务器可以等到收到所有客户端的消息后再写响应,或者服务器和客户端可以实现“ping-pong”:服务器收到请求,然后发回响应,然后客户端根据响应发送另一个请求,依此类推。

截止日期/超时(deadline/timeout) gRPC允许客户端指定他们愿意等待RPC完成多长时间,然后RPC会因DEADLINE_EXCEEDED错误而终止。在服务器端,服务器可以查询特定RPC是否超时,或者完成RPC还剩多少时间。

截止日期或超时的指定方式因语言而异-例如,并非所有语言都有默认截止日期,有些语言API根据截止日期(固定时间点)工作,有些语言API根据超时(持续时间)工作。

RPC终端 在gRPC中,客户端和服务器都独立地自行确定调用是否成功,他们的结果可能不一致。这意味着,例如,您可以在服务器端成功完成RPC (“我已经发送了我的所有回复!”)但是在客户端失败了(“回复在我的截止日期之后到达!”)中。服务器也可以在客户端发送所有请求之前决定完成。

取消RPCs 客户端或服务器可以随时取消RPC,取消会立即终止RPC,这样就不会做进一步的工作。注意这不是“undo”:取消之前所做的更改不会回滚。

元数据(Metadata) 元数据是关于特定RPC调用的信息(如身份验证详细信息),以键值对列表的形式,其中键是字符串,值通常是字符串(但可以是二进制数据)。元数据对gRPC本身是不透明的-它允许客户端向服务器提供与调用相关的信息,反之亦然。

元数据的访问依赖于语言。

信道 gRPC信道提供到指定主机和端口上的gRPC服务器的连接,并在创建客户端存根(或某些语言中的“客户端”)时使用。客户端可以指定信道参数来修改gRPC的默认行为,例如打开和关闭消息压缩。信道有状态,包括连接和空闲。

gRPC如何处理关闭频道取决于语言。一些语言也允许查询通道状态。

gRPC 通讯原理

基于HTTP/2 HTTP/2 提供了连接多路复用、双向流、服务器推送、请求优先级、首部压缩等机制。可以节省带宽、降低TCP链接次数、节省CPU,帮助移动设备延长电池寿命等。gRPC 的协议设计上使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。

RPC采用客户端/服务器模式。请求程序就是一个客户端,而服务提供程序就是一个服务器。首先,客户端调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行,具体可以参考下图。

gRPC 框架介绍

在使用过程当中只需要引用以上几个库即可。

gRPC Demo编写(服务端、客户端)

1.建项目(项目结构为client、service。)

2.生成proto文件

juster zhu:Google Protocol buffer3.0 in c#zhuanlan.zhihu.com

3.生成pem文件,不需要加密可跳过这个步骤

juster zhu:.NetCore3.1 gRPC pem证书使用zhuanlan.zhihu.com

4.服务端代码

Data/TempDataBase.cs :模拟数据库。

Protos/message.proto:定义好的proto文件,文件里包括了通讯用到的实体类以及方法。

Services/MyMsgService.cs :定义好的服务方法具体的实现类。

cacert.pem 和 privkey.pem 是加密所需要的用到的证书文件。

Program.cs 启动类。

注意事项:

proto文件里定义方法c#是不能直接调用的,所以微软这块封装的特别好我们只需要把proto文件stub classes的选项选为server only即可根据proto中定义的内容生成服务端的c#方法。生成的内容通常为xxxxBase结尾。

那么我们F12跟进去查看一下具体(c#)实现。

有兴趣的小伙伴可以研究一下,如果想进一步研究这里面的实现和原理建议用java或者go语言版本的grpc的库,是可以了解到更多过程的。

启动类代码:

5.客户端代码

客户端结构相对简单一点。这里参考服务端的结构介绍大致都相同。

启动类代码:

与服务端交互的代码实现:

代码语言:javascript复制
        public static async Task GetByNo(MessageService.MessageServiceClient client)
        {
            var res = await client.GetByNoAsync(new GetMsgByNoRequest() { Id = 1 });
            var msg = res.Msg;
            Console.WriteLine($"Get response messages : id-{ msg.Id },name-{ msg.Name },msg-{ msg.Msg }.");
        }

        public static async Task GetAllAsync(MessageService.MessageServiceClient client)
        {
            using var call = client.GetAll(new GetAllMsgRequest());
            var responseStream = call.ResponseStream;
            while (await responseStream.MoveNext())
            {
                Console.WriteLine(responseStream.Current.Msg);
            }
        }

        public static async Task AddPhoto(MessageService.MessageServiceClient client)
        {
            Metadata md = new Metadata { { "id","2004" } };

            FileStream fs = File.OpenRead("1.jpg");
            using var call = client.AddPhoto(md);
            var stream = call.RequestStream;
            while (true)
            {
                byte[] buffer = new byte[128 * 1024];
                int numRead = await fs.ReadAsync(buffer,0,buffer.Length);
                if (numRead == 0)
                {
                    break;
                }

                if (numRead < buffer.Length)
                {
                    Array.Resize(ref buffer,numRead);
                }
                await stream.WriteAsync(new AddPhotoRequest { Data = ByteString.CopyFrom(buffer) });
            }
            await stream.CompleteAsync();
            var res = await call.ResponseAsync;
            Console.WriteLine(res.IsOk);
        }

        public static async Task SaveAll(MessageService.MessageServiceClient client)
        {
            //初始化待发送数据集
            var messages = new List<JusterMessage>()
            {
                new JusterMessage
                {
                    Id = 100,
                    Name = "zhangsan",
                    Msg = "123456789"
                },
                 new JusterMessage
                {
                    Id = 200,
                    Name = "json",
                    Msg = "3333333"
                },
                 new JusterMessage
                {
                    Id = 300,
                    Name = "moon",
                    Msg = "5555555"
                }
            };

            //创建双向stream
            using var call = client.SaveAll();
            //请求
            var requestStream = call.RequestStream;
            //响应
            var responseStream = call.ResponseStream;

            //开接收线程接收服务器回传数据
            var responseTask = Task.Run(async()=>
            {
                while (await responseStream.MoveNext())
                {
                    Console.WriteLine($"Saved: { responseStream.Current.Msg }");
                }
            });

            //向服务器发送请求
            foreach (var m in messages)
            {
                await requestStream.WriteAsync(new MsgRequest { Msg = m } );
            }
            //通知服务器请求发送完毕
            await call.RequestStream.CompleteAsync();
            //等待接收线程结束。注意responseTask的await需要放到最后不然结果永远走不完。
            await responseTask;
        }

运行效果:

第一步我们先设置双启动

记住service是要在client前面启动的,通过右侧的上下箭头调整顺序。

到这里大致我们对gRPC的框架有些基础的认识,下一章介绍,gRPC中的身份验证。

0 人点赞