dotnet core grpc

原文在本人公众号中,欢迎关注我,时不时的会分享一些心得

http和rpc是现代微服务架构中很常用的数据传输方式,两者有很多相似之处,但是又有很大的不同。http是一种规范性、通用性、非常标准的传输协议,几乎所有的语言都支持,如果要确保各平台无缝衔接,可以考虑使用http协议,例如现在常规的restful,整个传输过程通常使用json数据格式。以至于不管是前端还是后端都可以很好的对接。
rpc协议不仅仅是服务间通信协议,甚至是进程间也存在。可以降低诸多微服务之间调用成本,屏蔽了通讯细节,调用远程方法处理数据时像调用本地方法一样丝滑顺畅。但是对前端和浏览器不是特别友好。
grpc是google制定实现的一种rpc形式,官网解释是:

grpc是可以在任何环境中运行的现代开源高性能rpc框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。

grpc主要包括四个特点

  1. 简单的服务定义且不局限于语言:grpc基于protobuf协议构建,该协议提供了强大的系列化工具和语言定义服务。
  2. 跨语言和平台:可自动为多语言或平台生成客户端和服务端内容。
  3. 快速启动和扩展性:只需极少代码就可以生成整个通讯环境,可以扩展数百万rpc请求。
  4. 双向流和集成身份验证:基于http/2的传输机制以及双向流和而完全可集成的插入式身份验证机制。
    protobuf协议
    该协议是一种序列化结构化数据的灵活,高效,自动化的机制,比xml更小,更快,更简单。您定义要一次构造数据的方式,然后可以使用生成的特殊源代码轻松地使用各种语言在各种数据流中写入和读取结构化数据。您甚至可以更新数据结构,而不会破坏已针对“旧”格式编译的已部署程序。
    协议文件定义方式:

    这是一个简单的定义,此文件在.proto文件中定义,与语言无关。每个消息类型都有一个或多个唯一编号的字段。每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串等。定义好后可以使用编译器生成对应语言的操作对象。
    更多协议内容请参阅:。

    .net core3.0 中的grpc

  5. 使用visual stduio 2019 创建一个grpc服务

    请注意:此处创建的是使用asp.net core的grpc服务。也就是说,有webhost去托管服务的。

    后面,我们使用host通用主机进行托管。

  6. 先来看看startup类

    图中这两行代码是关键,services.addgrpc();将rpc注入到应用程序服务中,
    endpoints.mapgrpcservice ();则是在中间件启用监听rpc请求。
    默认情况下此时生成项目后,proto文件只会生成serve的代码。客户端代码生成需要单独做配置。
    微软文档中是将proto文件拷贝到client中,去生成client代码。此时服务和客户端生成配置就不一样了:

<itemgroup>
    <protobuf include="protos\greet.proto" grpcservices="client" />
 </itemgroup>

我本人习惯将生成的server和client放在一起,打包后供client端和server端使用,只需要这样设置:

<itemgroup>
    <protobuf include="protos\greet.proto" grpcservices="server;client" />
 </itemgroup>

配置好后重新生成项目,grpctool会自动生成服务端,客户端代码。

client调用方式(asp.net core grpc):

var channel = grpcchannel.foraddress("https://localhost:5001");
var client = new greeter.greeterclient(channel);
var response = await client.sayhello(
    new hellorequest { name = "world" });
console.writeline(response.message);

接下来,我们创建一个简单的示例

  1. 创建两个个core控制台项目并添加文件夹protos和services,分别新增一个demo.proto文件和demoservice.cs文件
  2. 引入nuget包:
 <packagereference include="google.protobuf" version="3.10.1" />
    <packagereference include="grpc" version="2.24.0" />
    <packagereference include="grpc.tools" version="2.24.0">
      <privateassets>all</privateassets>
      <includeassets>runtime; build; native; contentfiles; analyzers; buildtransitive</includeassets>
    </packagereference>
    <packagereference include="microsoft.extensions.dependencyinjection" version="3.0.0" />
    <packagereference include="microsoft.extensions.hosting" version="3.0.0" />
  1. 编辑csproj文件:使得支持同时生成client和server代码(作为demo,server端 应该只作为server。生成client和server的代码的应该单独抽出来)
  2. 服务端:重写grpcexampleservice.grpcexampleservicebase中的rpc方法(也就是.proto文件生成的代码内容中的rpc服务方法)
public class demoservice: grpcexampleservice.grpcexampleservicebase
    {
        public override task<askresponse> ask(askrequest request, servercallcontext context)
        {
            return task.fromresult(new askresponse {content = "hello from ask"});
        }

        public override task<responsemodel> getname(requestmodel request, servercallcontext context)
        {
            return task.fromresult(new responsemodel { name = "hello  pluto" });
        }
    }
  1. 接下来就是创建启动项(host)了(grpcserver):
    这次使用通用主机,不适用asp.net core webhost。
public class grpcserver:ihostedservice
    {
        private readonly grpcexampleservice.grpcexampleservicebase _sampleservicebase;


        public grpcserver(grpcexampleservice.grpcexampleservicebase sampleservicebase)
        {
            _sampleservicebase = sampleservicebase;
        }

        /// <summary>
        /// 启动自己定义的rpc服务
        /// </summary>
        /// <param name="cancellationtoken"></param>
        /// <returns></returns>
        public task startasync(cancellationtoken cancellationtoken)
        {
            return task.factory.startnew(() =>
            {
                grpcservicemanager.start(grpcexampleservice.bindservice(_sampleservicebase));
            }, cancellationtoken);
        }

        /// <summary>
        /// 停止
        /// </summary>
        /// <param name="cancellationtoken"></param>
        /// <returns></returns>
        public task stopasync(cancellationtoken cancellationtoken)
        {
            return task.factory.startnew(() =>
            {
                grpcservicemanager.stop();
            }, cancellationtoken);
        }
    }

grpcservicemanager中就是真正的启动和停止grpc服务:

public class grpcservicemanager
    {
        static server server;
        public static void start(serverservicedefinition bindservice)
        {
            if (bindservice == null)
                throw new argumentnullexception("bindservice");
            var services = new list<serverservicedefinition>() { bindservice };
            start(services); //todo 添加配置,拦截器等等
        }
        private static void start(list<serverservicedefinition> services)
        {
            try
            {
                server = new server()
                {
                    ports = { new serverport("0.0.0.0", 8890, servercredentials.insecure) }
                };

                foreach (var service in services)
                {
                    server.services.add(service);
                }
                server.start();
                //todo consul 注册
            }
            catch (exception e)
            {
                console.writeline(e);
                throw;
            }
        }

        public static void stop()
        {
            try
            {
                server?.shutdownasync().wait();
            }
            catch (exception e)
            {
                console.writeline(e);
                throw;
            }
            
        }
    }

注意:这里应该将proto协议 服务端客户端端进行分离,协议生成的代码打包后 被客户端项目和服务端项目引用,这里只是为了简便客户端直接会引用服务,因为要用到grpcexampleservice.grpcexampleserviceclient

上边的todo 后再后续增加对应的功能。
这样服务端就创建完了。接下来就是创建客户端进行调用:

控制台客户端:
main中实现调用逻辑

static void main(string[] args)
        {
            var  channel=new grpc.core.channel("127.0.0.1",8890,channelcredentials.insecure);
            var democlient=new grpcexampleservice.grpcexampleserviceclient(channel);
            var res= democlient.ask(new askrequest
            {
                cate = 0,
                key = "1312"
            });
            var res2 = democlient.getname(new requestmodel
            {
                key = "1313"
            });
            console.writeline($"ask={res}.getname={res2}");
        }

然后就可以了,启动服务端,然后再启动客户端,就可以看到调用结果了。