什么是grpc
首先我们需要了解,什么是grpc
gRPC(全称:g oogle r emote p rocedure call)是由Google开发的一个高性能、开源的远程过程调用(RPC)框架。它基于 HTTP/2 协议,并且使用 Protocol Buffers(Protobuf)作为接口定义语言,提供了不同系统之间高效、安全的通信方式。gRPC 支持多种语言(包括 Go、C++、Java、Python 等),并且提供了丰富的功能,比如流式处理、双向通信、负载均衡等。
前置条件,环境配置
我们之前简单地讲过关于它的配置:GO语言 使用protobuf-CSDN博客
这里就不过多的赘述了
protobuf语法
我们之前只简单的讲过如何写一个简单的proto文件
这里我们深入一点的讲解它的语法
我们来看一个简单的文档
syntax = "proto3"; // 语法版本
message Person {
string name = 1; // 字符串类型字段,编号为 1
int32 age = 2; // 32 位整数,编号为 2
bool is_student = 3; // 布尔类型,编号为 3
}
syntax这个用来指定版本,我们现在学的都是proto3的版本
message Person由于我们使用的是go语言,这里我们就根据go的数据类型来讲
它定义了一个类似于go语言的struct结构体,结构体内部定义了数据的类型、名字、编号(编号不是值,只是编号没有赋值给变量)
它的数据类型有
|---------|-----------|
| 类型 | 说明 |
| int32 | 32位整数 |
| int64 | 64位整数 |
| uint32 | 无符号32位整数 |
| uint64 | 无符号64位整数 |
| sint32 | 有符号32位整数 |
| sint64 | 有符号64位整数 |
| fixed32 | 四字节固定长度整数 |
| fixed64 | 八字节固定长度整数 |
| float | 32位浮点数 |
| double | 64位浮点数 |
| bool | 布尔值 |
| string | 字符串 |
| bytes | 二进制数据 |
嵌套
同时,我们也可以在message里面嵌套另一个message
数组
proto里面定义数组的格式为:repeated 类型 name = 编号
转化为go代码后就是切片,并且长度和容量默认是0,我们都知道的切片存储的数据数量如果超出了容量,那么切片的容量就会翻倍
容量变化 : 0 - 1 - 2 - 4 - 8 - 16 - 32如果容量>1024,那么就不会翻倍,而是*1.25
Map
proto允许键值对的形式,即map,格式为:map<key类型,value类型> name = 编号
转化为go代码后也是这样的,因为go本身就有map类型
server
这个类型非常重要它的格式是:server name{ rpc name(a) returns(b) }
a和b都要是本文件内的message(或通过 import
正确导入的其他 proto 文件中的 message)
通过已有的proto文件生成go文件
这部分我们需要使用的命令是
protoc -I . go_out=. go-grpc_out=. helloworld.proto
|------------------|-------------------------------------|
| 指令 | 作用 |
| -I . | 指定搜索当前目录下文件 |
| go_out=. | 生成 Protobuf 序列化/反序列化代码,存放在当前目录下 |
| go-grpc_out=. | 生成 gRPC 服务代码,存放在当前目录下 |
| helloworld.proto | 文件名,可以指定单个也可以使用 *.proto指定所有当前目录下文件 |
对于生成文件存放的位置这里要注意
它是由两个因素组成的:1 proto文件内的option go_package指定的目录 2 go_out=. go-grpc_out=.命令行指定的目录
假如我的option go_package="a/b/c/d ;pb" 但是go_out=. go-grpc_out=.却指定当前目录下。最终文件生成位置是使用go_out=. go-grpc_out=.命令下创建一个目录d存放生成文件。pb是生成go文件的包的名字
生成的go文件里面都有哪些内容呢?
首先就是我们定义的message,它被转换为了go里面的结构体,还有该结构体的方法
以及server,它被抽象为一个接口,接口下有一些方法(rpc声明的)。我们想在go文件内使用这个接口,就需要定义一个结构体去实现该接口的所有方法。
假设我有一个proto文件
syntax = "proto3"; option go_package = ".;pb"; service Greeter{ rpc SayHi(One) returns (Two); } message One{ string name = 1; int64 Age = 2; } message Two{ string Return = 1; }
这里要注意:使用protoc -I . go_out=. go-grpc_out=. helloworld.proto会生成两个文件,一个是helloworld.pb.go另一个是helloworld_grpc.pb.gp
两个文件存储的内容是不同的
helloworld.pb.go存储
部分 说明 示例 消息结构体 对应 proto 中的 message type Request struct{...}
字段访问方法 Getter 方法 func (x *Request) GetName() string
序列化方法 二进制编解码 func (x *Request) Marshal() ([]byte, error)
工具方法 重置/比较等 func (x *Request) Reset()
描述符 元信息 var file_helloworld_proto_rawDesc
helloworld_grpc.pb.gp存储
部分 说明 示例 客户端接口 调用远程服务的存根 type GreeterClient interface
服务端接口 需要实现的方法 type GreeterServer interface
注册函数 服务端注册 func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer)
方法描述符 RPC 方法元数据 _Greeter_SayHello_Handler
流式接口 流式 RPC 支持 type Greeter_SayHelloServer interface
这个文件里面定义了两个message,当我们生成go文件的时候它会变成struct结构体。Greeter会生成对应的接口,我们可以通过生成的go文件的结构体生成一个新的结构体对象,让这个对象实现接口的方法,这样这个结构体对象就实现了接口
如何实现grpc
我们现在已经有proto文件生成的go文件了,我们再来看如何实现服务端和客户端。
我们的想法是:客户端自己没有实现某方法的函数,但是可以调用服务端的函数进行操作
大致的过程是:
client调用server的服务,server完成处理后再返回给client
服务端
我们来看server的编写,这里我们依旧采用上面生成的go文件
首先我们需要有一个服务端的实例
我们使用:g := grpc.NewServer()
然后我们需要注册服务到这个服务端内,那我们注册的服务是什么呢?其实就是实现了接口的结构体
我们首先创建一个结构体
type Server struct { pb.UnimplementedGreeterServer }
这里的pb是我们导入的生成的go文件,它属于这个包。我么就用这个
因为这个结构体要实现接口,接口内存在一个方法SayHi,所以结构体要实现这个方法
func (s *Server) SayHi(ctx context.Context, request *pb.One) (*pb.Two, error) { return &pb.Two{ Return: "nihao" + request.Name + "今年", }, nil }
这里的逻辑是自己写的,但是形参实参都要严格按照我们生成的go文件来写
ctx context.Context这个形参是一定要写并且一定要在第一位的,后面的形参是我们在写proto文件的时候就定义好的。返回值也是,只不过后面多了一个error类型的返回值。
这样,结构体Server就实现了接口,我们就可以把它注册到服务端了
pb.RegisterGreeterServer(g, &Server{})
这个注册的方法也是生成的go文件里面自动生成的。我们直接使用即可
这里的registerGreeterServer也是有说法的,我们在proto文件里面定义的server叫Greeter,所以这里就是RegisterGreeterServer,如果叫Nihao,那就是RegisterNihaoServer
然后我们就开始编写一个监听器,用于监听网络请求
lis, err := net.Listen("tcp", "localhost:8080")
这里就是内置的net包创建的一个监听器,然后把监听器和注册的服务联系到一起
err = g.Serve(lis)
这样,当监听到对应ip的网络请求的时候就可以跳转到对应的服务了,这样一个简单的服务端就写完了
客户端
我们再来写客户端(发出请求的一方)
这里需要知道的是,它们需要有相同的proto文件以及生成的go文件
在这里就不需要实现什么接口之类的了,我们只需要做:建立连接,发送请求这两步
首先是建立连接:
需要提前导入"google.golang.org/grpc"
conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
这里的localhost:8080是我们在服务端里面设置的,这样一个grpc的请求就建立完成了
既然有连接,那就需要有关闭连接的时候。我们使用defer conn.Close()关闭连接
c := pb.NewGreeterClient(conn)
r, err := c.SayHi(context.Background(), &pb.One{ Name: "xxx", Age: 12, })
我们创建一个客户端实例c
我们通过c调用SayHi方法,这个方法虽然比较简单,但是我们如果有能力完全可以换成很复杂很有用的业务逻辑。
r是服务端返回的数据
这样一个客户端就创建好了,我们先运行服务端再运行客户端就可以发现r正确的接收到了服务端的返回信息。