首页 > 技术文章 > gRPC的简单使用

catcher1994 2019-08-10 22:34 原文

前言

八月初的时候,在公司内部做了一个主题为《gRPC的简单使用》的分享,其实就是和小伙伴们扯扯淡,现在抽空回忆一下,也算是一个小小的总结吧。

现在市面上耳熟能详的RPC框架也很多,下面列举几个遇到比较多的。

  1. 谷歌的gRPC
  2. 推特的Thrift
  3. 阿里的Dubbo
  4. 。。。。

它们都是支持多语言的,相对来说,这三个之中,Dubbo支持的语言略微少一点。现在在一个公司内都能见到多种语言的技术栈都已经是十分常见的事了,好比我司,都有JAVA,C#,Python三种语言了,所以在多语言支持这方面,在技术选型的时候,肯定是要有所考虑的。

下面进入正式的主题,gRPC。

gRPC的简单介绍

gRPC是一个现代的开源高性能RPC框架,可以在任何环境中运行。它可以高效地将数据中心内和跨数据中心的服务连接起来,并支持可插拔的负载平衡、跟踪、健康检查和身份验证。同时,它还把设备,移动应用程序和浏览器连接到后端服务的分布式计算变得很容易。

gRPC有什么优点呢?

  1. 简单的服务定义 (使用Protocol Buffers定义服务,这是一个功能强大的二进制序列化工具集和语言)
  2. 跨语言和平台工作 (在微服务式架构中有效地连接多语言服务(10+种语言支持)并能自动为各种语言和平台的服务生成惯用的客户端和服务器存根)
  3. 快速启动并扩展 (使用单行安装运行时和开发环境,并使用框架每秒扩展到数百万个RPC)
  4. 双向流媒体和集成的身份验证 (双向流媒体和集成的身份验证 基于http/2的传输的双向流和完全集成的可插拔身份验证)

gRPC在使用的时候有4种模式供我们选择

  1. 一元RPC(Unary RPCs ):这是最简单的定义,客户端发送一个请求,服务端返回一个结果
  2. 服务器流RPC(Server streaming RPCs):客户端发送一个请求,服务端返回一个流给客户端,客户从流中读取一系列消息,直到读取所有消息
  3. 客户端流RPC(Client streaming RPCs ):客户端通过流向服务端发送一系列消息,然后等待服务端读取完数据并返回处理结果
  4. 双向流RPC(Bidirectional streaming RPCs):客户端和服务端都可以独立向对方发送或接受一系列的消息。客户端和服务端读写的顺序是任意。

我们要根据具体的场景来决定选择那一种。

这里只介绍一元RPC。正常来说,一元RPC应该可以满足我们日常60~70%的需求了吧。

基本用法

gRPC的基本用法可以简单的分为三个点:

  • 服务的定义,即proto文件的编写
  • 服务端代码编写
  • 客户端代码编写

下面我们依次来看一下

服务的定义

既然要定义一个服务,肯定是知道了这个服务要完成什么事之后。

在定义之前,要对proto3和proto2有所了解。不过proto3是推荐的格式。所以我们基本上只要用proto3就可以了。

下面先来看一个后面要用到的proto文件。

syntax = "proto3";

option csharp_namespace = "XXXService";

package UserInfo;

service UserInfoService {
  rpc GetList(GetUserListRequest) returns (GetUserListReply){}
  rpc GetById(GetUserByIdRequest) returns (GetUserByIdRelpy){}
  rpc Save(SaveUserRequest) returns (SaveUserReply){}
}


message GetUserByIdRequest {
	int32 id = 1;
}

message GetUserByIdRelpy{
	int32 id = 1;
	string name = 2;
	int32 age = 3;
	int64 create_time = 4;
}

message GetUserListRequest {
	int32 id = 1;
	string name = 2;	
}

message GetUserListReply {
  message MsgItem {
    int32 id = 1;
	string name = 2;
	int32 age = 3;
	int64 create_time = 4;
   }
   int32 code = 1;
   string msg = 2;
   repeated MsgItem data = 3;
}

message SaveUserRequest {
	string name = 1;
	int32 age = 2;	
}

message SaveUserReply {
   int32 code = 1;
   string msg = 2;
}

它有下面的几个部分

  1. syntax , 指定要用那个版本的语法
  2. service , 指定rpc服务的接口,简单理解成我们平时定义的接口
  3. message , 指定要传输的消息体,简单理解成我们平常用的 DTO
  4. package , 指定包名
  5. option , 可选参数的定义,不同语言有不同的选项

其实看上去还是比较容易懂的。至少一眼看过去能知道是些什么意思。

如果对proto3还没有了解的,可以参考这个文档Language Guide (proto3),里面很清楚的介绍了一些数据类型和不同语言数据类型的对应关系。

这里有一个要注意的是,时间类型,在proto3中,没有datetime类型,过去很长一段时间,我们是只能用时间戳来表示时间,也就是定义一个长整型,现在是可以用timestamp表处理了。

在写服务端和客户端代码之前,我们需要根据proto文件生成对应的代码。

一个命令即可搞定。

protoc --proto_path=IMPORT_PATH \
           --cpp_out=DST_DIR \
           --java_out=DST_DIR \
           --python_out=DST_DIR \
           --go_out=DST_DIR \
           --objc_out=DST_DIR \
           --csharp_out=DST_DIR \
           path/to/file.proto

现在时代进步的这么快,不少语言已经有工具做了集成,可以在build项目的时候就生成对应的文件了,不需要我们再单独去执行一次上面的那个命令。

好比我们的.NET项目,可以在ItemGroup中直接指定Protobuf,然后告诉它,proto文件是那个,是要生成服务端代码还是客户端代码。

可以看看下面这个具体的例子。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\userinfo.proto" GrpcServices="Server" />
  </ItemGroup>
  
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
    <PackageReference Include="Google.Protobuf" Version="3.8.0" />
    <PackageReference Include="Grpc.Core" Version="1.22.0" />
    <PackageReference Include="Grpc.Tools" Version="1.22.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

再往下,就是写代码了。

服务端代码编写

服务端代码分两部分,一部分是服务具体的实现,一部分是服务怎么起来。

先来看看服务的具体实现。

namespace MyBasedServiceA
{
    using Grpc.Core;
    using System.Linq;
    using System.Threading.Tasks;

    public class UserInfoServiceImpl : UserInfoService.UserInfoServiceBase
    {
        public override Task<GetUserByIdRelpy> GetById(GetUserByIdRequest request, ServerCallContext context)
        {
            var result = new GetUserByIdRelpy();

            var user = FakeUserInfoDb.GetById(request.Id);

            result.Id = user.Id;
            result.Name = user.Name;
            result.Age = user.Age;
            result.CreateTime = user.CreateTime;

            return Task.FromResult(result);
        }

        public override Task<GetUserListReply> GetList(GetUserListRequest request, ServerCallContext context)
        {
            var result = new GetUserListReply();

            var userList = FakeUserInfoDb.GetList(request.Id, request.Name);

            result.Code = 0;
            result.Msg = "成功";
            result.Data.AddRange(userList.Select(x => new GetUserListReply.Types.MsgItem
            {
                Id = x.Id,
                Age = x.Age,
                CreateTime = x.CreateTime,
                Name = x.Name
            }));

            return Task.FromResult(result);
        }

        public override Task<SaveUserReply> Save(SaveUserRequest request, ServerCallContext context)
        {
            var result = new SaveUserReply();

            var flag = FakeUserInfoDb.Save(request.Name, request.Age);

            result.Code = 0;
            result.Msg = "成功";
            
            return Task.FromResult(result);
        }
    }
}

可以看到上面的代码,我们只要继承由proto文件生成的一个基类,然后去重写它的实现,就可以认为是实现了一个服务。这个其实就是写我们具体的业务逻辑,大boss有什么需求,堆上去就好了。

然后来看第二部分,服务怎么起来。

在这里我选择的方案是使用通用主机来跑。当然也可以直接在Startup的Configure方法中去启动服务。只要能起来就行

推荐阅读