什么?你还不了解gRPC,我不允许,开始启动你的RPC服务吧(1)

前言

Hi,我是老胡,今天我在学习grpc的时候,特此记下本博客,详尽的介绍了Go语言中最流行的RPC框架,并且展示其中的技术栈,话不多说,开造!


1 gRPC介绍


1.1 gRPC是什么牛马?

在了解gRPC之前,我们首先需要了解什么是RPC。


1.2 RPC是什么牛马?

官方的来说,RPC指的就是远程过程调用,它的调用包括传输协议和编码协议等,允许运行于一台计算机上的程序调用另一台计算机上的子程序,而开发人员无需额外为这交互作用编程,就像对本地函数进行调用一样方便,简单的理解就是计算机1的程序1通过RPC远程调用计算机2的程序2可以直接得到调用后的结果,理解“本地函数”


1.3 牛马gRPC?

gRPC顾名思义是一个高性能、开源、通用的RPC框架,有c、java、go版本,本框架基于HTTP/2标准设计,拥有双向流、流控、头部压缩、单TCP连接上的多复用请求等特性,gRPC的接口描述语言使用的是Protobuf


1.4 GRPC调用模型展示

  • 客户端在程序中调用方法,发起RPC调用
  • 对请求信息使用Protobuf进行对象序列化压缩
  • 服务端接受请求后,解码请求体,进行业务逻辑处理后并且返回
  • 对响应结果使用Protobuf进行对象序列化叶索
  • 客户端接受服务端响应后,解码请求体,回调被调用的A方法,唤醒正在等待响应的客户端调用并且返回响应结果

2 Protobuf介绍


2.1 什么是Protobuf

Protobuf是一种与语言、平台无关,且可拓展的序列化结构化数据的数据描述语言,简称IDL,用于通信协议、数据存储等,与JSON,XML对比小而块

基本语法:

//声明使用的是proto3语法,如果不声明,默认是proto2
syntax = "proto3";

package gRPC;

//定义名为Greeter的RPC服务,入参为HelloRequest 消息体,出参为HelloReply 消息体
service Greeter {
	rpc SayHello (HelloRequest) returns (HelloReply) {}
}

//消息体,每一个消息体的字段都包含三个属性:类型、字段名称、字段编号
message HelloRequest {
	string name = 1;
}

message HelloReply {
	string message = 1;
}

编写完.proto文件以后,会生成对应语言的proto文件,这时候Protobuf编译器会根据选择的语言和调用的插件情况,生成相应语言的Service Interface Code 和Stubs。

需要注意的是Protobuf生成的数据类型与原始类型并不完全一致,对应可以查看表


3 gRPC优缺点


3.1 gRPC与RESTful API对比

特性gRPCRESTful API
规范必须.proto可选OpenAPI
协议HTTP/2任意版本的HTTP协议
有效载荷Protobuf 小 二进制JSON 大,方便读
浏览器支持否(需要grpc-web)
流传输客户端、服务端、双向客户端,服务端
代码生成OpenAPI+第三方工具

4 Protobuf使用


4.1 安装Protobuf

在gRPC开发过程中,我们需要与Protobuf打交道,在编写.proto文件后,需要使用到一个编译器,就是protoc,protoc是Protobuf的编译器,C++ 编写,注意是编译.proto文件的

安装命令

wget https://github.com/google/protobuf/releases/download/v3.11.2/protobuf-all-3.11.2.zip
unzip protobuf-all-3.11.2.zip
cd protobuf-all-3.11.2
./configure
make 
make install

检查安装是否成功

protoc --veriosn

如果发生报错:错误加载共享库

我们需要在命令行执行ldconfig命令,这是因为在安装protoc的时候,我们安装了一个新的动态链接库,而ldconfig命令一般默认在系统启动的时候运行,所以在特定的情况下是找不到这个新安装的动态链接库的,所以需要手动的执行ldconfig命令,让动态链接库为系统所共享,ldconfig命令是一个动态链接库管理命令


4.2 安装protoc插件

仅仅安装编译器是不够的的,还需要安装针对不同语言下的运行时的protoc插件,而对应Go语言的是protoc-gen-go插件,安装命令如下

go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.3

之后就会在/pkg/mod下载好对应的源码

go install github.com/golang/protobuf/protoc-gen-go

会自动的在bin目录下产生protoc-gen-go可执行文件

需要保证这个bin目录已经配置在系统的环境变量里面才可以(类型jdk的bin可执行目录配置),因为需要直接运行protoc-gen-go插件


在这个过程中可能会遇到一些问题,需要go的protoc-gen-go,但是因为golang被墙,go get github.com/golang/protobuf/protoc-gen-go的时候总是timeout。

个人解决办法

  • 第一种使用代理下载(比较推荐,不会缺少依赖)
  • 第二种使用源码安装。去github 去下载源码,然后 go install grpc
方法一
使用代理安装的方法
开启mod 模式 set GO111MODULE=on
设置代理 set GOPROXY=https://mirrors.aliyun.com/goproxy/ (设置成阿里云代理)
go get google.golang.org/grpc
方法二
cd $GOPATH/src 目录下
git clone https://github.com/grpc/grpc-go ./google.golang.org/grpc
git clone https://github.com/golang/net.git ./golang.org/x/net
git clone https://github.com/google/go-genproto.git ./google.golang.org/genproto
git clone https://github.com/golang/text.git ./golang.org/x/tex
go install google.golang.org/grpc

我此处使用的是方法一,由于使用的是gomod模式,下载好的源码是放在pkg/mod下的

4.3 初始化demo项目

初始化一个gRPC专用的demo,用于演示后续的gRPC和Protobuf应用,执行下面的命令

mkdir -p go-programming-tour-book/grpc-demo
cd go-programming-tour-book/grpc-demo
go mod init github.com/go-programming-tour-book/grpc-demo

然后创建好对应的项目目录

在这里插入图片描述

4.4 编译和生成proto文件

  1. 在proto里面新建helloworld.proto文件,写入下面的声明
syntax = "proto3";

package helloworld;

option go_package = "./proto;helloworld";
//定义名为Greeter的RPC服务,入参为HelloRequest 消息体,出参为HelloReply 消息体
service Greeter {
        rpc SayHello (HelloRequest) returns (HelloReply) {}
}

//消息体,每一个消息体的字段都包含三个属性:类型、字段名称、字段编号
message HelloRequest {
        string name = 1;
}

message HelloReply {
        string message = 1;
}

//option go_package = "path;name";
 
//path 表示生成的go文件的存放地址,会自动生成目录的。
//name 表示生成的go文件所属的包名
  1. 生成proto文件

在项目的根目录下,执行protoc的相关命令,生成对应的pb.go文件,命令如下:

 protoc --go_out=plugins=grpc:. ./proto/*.proto
  • go_out:设置所生成的Go代码的输出目录,该指令会加载protoc-gen-go插件,以达到生成go代码的目的,这里的:是分隔符的作用,后面跟着命令所需要的参数,意味着把生成的go代码输出到指向的protoc编译的当前目录
  • plugins=plugin1+plugin2:指定要加载的子插件列表,我们定义的proto文件是设计了RPC服务的,而默认是不会生成RPC代码的,所以这里需要给出grpc,将这个参数传递给protoc-gen-go插件,告诉编译器,请支持RPC

执行完之后,就可以看到对应的go代码了

4.5 生成的.pb.go代码

下面来研究一下代码里面有哪些功能?这个文件是实际在应用中会引用到的文件

//消息体,每一个消息体的字段都包含三个属性:类型、字段名称、字段编号
type HelloRequest struct {
        Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
        XXX_NoUnkeyedLiteral struct{} `json:"-"`
        XXX_unrecognized     []byte   `json:"-"`
        XXX_sizecache        int32    `json:"-"`
}

func (m *HelloRequest) Reset()         { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage()    {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
        return fileDescriptor_4d53fe9c48eadaad, []int{0}
}

func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
        return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
        return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
        xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
        return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
        xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}

var xxx_messageInfo_HelloRequest proto.InternalMessageInfo

func (m *HelloRequest) GetName() string {
        if m != nil {
                return m.Name
        }
        return ""
}

主要是涉及HelloRequest类型,包含一组Getter方法,该方法通过实现方法ProtoMessage,表示这是一个实现了proto.Message的接口,另外HelloReply类型也是一样的生成结果。

type HelloReply struct {
        Message              string   `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
        XXX_NoUnkeyedLiteral struct{} `json:"-"`
        XXX_unrecognized     []byte   `json:"-"`
        XXX_sizecache        int32    `json:"-"`
}

func (m *HelloReply) Reset()         { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage()    {}
func (*HelloReply) Descriptor() ([]byte, []int) {
        return fileDescriptor_4d53fe9c48eadaad, []int{1}
}

func (m *HelloReply) XXX_Unmarshal(b []byte) error {
        return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
        return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (m *HelloReply) XXX_Merge(src proto.Message) {
        xxx_messageInfo_HelloReply.Merge(m, src)
}
func (m *HelloReply) XXX_Size() int {
        return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
        xxx_messageInfo_HelloReply.DiscardUnknown(m)
}

var xxx_messageInfo_HelloReply proto.InternalMessageInfo

func (m *HelloReply) GetMessage() string {
        if m != nil {
                return m.Message
        }
        return ""
}

下面来看看.pb.go文件的初始化方法

func init() {
        proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
        proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
}

func init() { proto.RegisterFile("proto/helloworld.proto", fileDescriptor_4d53fe9c48eadaad) }

var fileDescriptor_4d53fe9c48eadaad = []byte{
        // 155 bytes of a gzipped FileDescriptorProto
        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2b, 0x28, 0xca, 0x2f,
        0xc9, 0xd7, 0xcf, 0x48, 0xcd, 0xc9, 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x03, 0x0b, 0x08,
        0x71, 0x21, 0x44, 0x94, 0x94, 0xb8, 0x78, 0x3c, 0x40, 0xbc, 0xa0, 0xd4, 0xc2, 0xd2, 0xd4, 0xe2,
        0x12, 0x21, 0x21, 0x2e, 0x96, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20,
        0x30, 0x5b, 0x49, 0x8d, 0x8b, 0x0b, 0xaa, 0xa6, 0x20, 0xa7, 0x52, 0x48, 0x82, 0x8b, 0x3d, 0x37,
        0xb5, 0xb8, 0x38, 0x31, 0x1d, 0xa6, 0x08, 0xc6, 0x35, 0xf2, 0xe4, 0x62, 0x77, 0x2f, 0x4a, 0x4d,
        0x2d, 0x49, 0x2d, 0x12, 0xb2, 0xe3, 0xe2, 0x08, 0x4e, 0xac, 0x04, 0xeb, 0x12, 0x92, 0xd0, 0x43,
        0x72, 0x01, 0xb2, 0x65, 0x52, 0x62, 0x58, 0x64, 0x0a, 0x72, 0x2a, 0x95, 0x18, 0x9c, 0x44, 0xa2,
        0x84, 0xf4, 0xf4, 0xc1, 0xae, 0xb5, 0x46, 0x28, 0x49, 0x62, 0x03, 0x8b, 0x18, 0x03, 0x02, 0x00,
        0x00, 0xff, 0xff, 0xb6, 0x29, 0x8a, 0xca, 0xd9, 0x00, 0x00, 0x00,
}

这里面的fileDescriptor_4d53fe9c48eadaad 表示的是一个经过编译后的proto文件,是对proto文件的整体描述,包含了文件名,引用内容,包名,选项设置…,,可以说整个proto文件的信息都可以在这里取到

同时,每一个Message Type中都包含了Descriptor方法,Descriptor方法指的是对一个消息体定义的描述,而这个方法会在fileDescriptor中寻找对应消息体的字段所在的位置后再进行返回,代码如下:

func (*HelloRequest) Descriptor() ([]byte, []int) {
        return fileDescriptor_4d53fe9c48eadaad, []int{0}
}

func (*HelloReply) Descriptor() ([]byte, []int) {
        return fileDescriptor_4d53fe9c48eadaad, []int{1}
}

下面来看我们的GreeterClient接口,因为Protobuf是客户端和服务端可共用的一份proto文件,因此除了数据描述信息,还包含客户端和服务端相关内部调用的接口约束和调用方式的实现,后续在多服务内部调用的时候会经常用到,代码如下:

type GreeterClient interface {
        SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
        cc grpc.ClientConnInterface
}

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
        return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
        out := new(HelloReply)
        err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
        if err != nil {
                return nil, err
        }
        return out, nil
}

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
        SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

// UnimplementedGreeterServer can be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}

func (*UnimplementedGreeterServer) SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
        return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
        s.RegisterService(&_Greeter_serviceDesc, srv)
}

func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
        in := new(HelloRequest)
        if err := dec(in); err != nil {
                return nil, err
        }
        if interceptor == nil {
                return srv.(GreeterServer).SayHello(ctx, in)
        }
        info := &grpc.UnaryServerInfo{
                Server:     srv,
                FullMethod: "/helloworld.Greeter/SayHello",
        }
        handler := func(ctx context.Context, req interface{}) (interface{}, error) {
                return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
        }
        return interceptor(ctx, in, info, handler)
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
        ServiceName: "helloworld.Greeter",
        HandlerType: (*GreeterServer)(nil),
        Methods: []grpc.MethodDesc{
                {
                        MethodName: "SayHello",
                        Handler:    _Greeter_SayHello_Handler,
                },
        },
        Streams:  []grpc.StreamDesc{},
        Metadata: "proto/helloworld.proto",
}

4.6 更多的数据类型支持

Protobuf本身就支持多种数据类型

  1. 通用类型,网上很常见
  2. 需要传递动态数组,在Protobuf中,可以使用repeated关键字实现,如果一个字段被声明为repeated,那么这个字段可以重复使用任意次,重复值的顺序将保留在Protobuf中,重复字段可被视为动态大小的数组,代码如下
message HelloRequest {
	repeated string name = 1;
}
  1. 嵌套类型

指的是在消息体里面又嵌套了其他的消息体

message HelloRequest {
	message World {
		string name = 1;
	}
	repeated World worlds = 1;
}

这一种是将World消息体定义在HelloRequest消息体里面,也就是说,其归属在消息体HelloRequest下,在调用的时候,需要使用HelloRequest.World这样的方式才可以在外部调用成功

下面这种是将World消息体定义在外部,推荐

message World {
	string name = 1;
}
message HelloRequest {
	repeated World worlds = 1;
}
  1. oneof

如果希望消息体可以包含多个字段,前提条件是最多同时只允许设置一个字段,则可以使用oneof关键字来实现,代码如下

message HelloRequest {
	oneof name {
		string nick_name = 1;
		string true_name = 2;
}
}
  1. enum

枚举类型,限定传入的字段值必须是预定义的值列表之一

enum NameType {
	NickName = 0;
	TrueName = 1;
}
message HelloRequest {
	string name = 1;
	NameType nameType = 2;
}
  1. map

需要设置键和值的类型,格式为map< key_type, value_type>map_field = N,

message HelloRquest {
	map<string, string> names = 2;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Devin Dever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值