同学们好!我们在前面为大家介绍了基于RPC协议的远程过程调用编程,以及基于ProtoBuf的序列化反序列化编程。在RPC客户机与服务器之间,我们可以采用Go语言自带的序列化机制,即GOB,传递调用和返回信息,也可以采用JSON格式的字符串实现数据交换,那么我们是否可以利用ProtoBuf的序列化反序列化机制,实现RPC客户机与服务器之间的信息传递呢?ProtoBuf除了可以帮助我们描述结构化数据以外,还能帮助我们做更多的事吗?既然RPC客户机与服务器的面向对象封装如此的模式化,那么是否有更加自动化的工具可以帮助我们生成RPC客户机与服务器的基础代码框架呢?在这节课里我要为大家介绍的gRPC框架将很好地回答这些问题。
在这节课里,我们将简单了解一下gRPC的基本常识、安装方法和使用技巧,然后基于gRPC框架,构建一个RPC服务器和一个RPC客户机。大家可以对比一下,使用gRPC和不使用gRPC构建RPC应用的差别。
首先,我们来了解一下关于gRPC的基本常识。
gRPC是一个非常轻量级的微服务框架。轻就轻在它只着意解决两个问题,一个是对远程过程调用的面向对象化封装,再一个就是数据的序列化和反序列化。针对第一个问题,gRPC可以根据ProtoBuf中对服务和数据的描述,自动生成RPC客户机和服务器的基础代码框架。针对第二个问题,gRPC可以利用ProtoBuf的序列化反序列化机制,在RPC客户机和服务器之间传递调用和返回信息。
作为第三方框架的gRPC需要单独安装才能使用。
在上一节课,为了让ProtoBuf编译器能够顺利地将扩展名为“.proto”的文件编译为扩展名为“.go”的文件,我们安装了ProtoBuf的Go语言代码生成器。但如果被编译的“.proto”文件中含有与RPC有关的描述,则还需要安装gRPC的Go语言代码生成器。同时为了让代码生成器生成的Go语言代码能够顺利地被Go语言编译器编译,并和我们编写的其它代码一起链接为可执行程序,我们还需要安装gRPC运行库。我们可以直接从GitHub上下载grpc-go项目,放到GOPATH的特定目录下,并在代码生成器目录下完成构建和安装。
为了保证gRPC运行库中的代码能够被正确执行,我们还需要安装一些其它依赖。
下面我将了解一下,在Go语言程序中,如何通过gRPC框架,实现远程过程调用。
使用gRPC框架实现远程过程调用的关键,是在扩展名为“.proto”的ProtoBuf脚本文件中,将服务、服务方法、参数消息和返回值消息描述清楚,而后借助ProtoBuf编译器,将该脚本文件编译为Go语言代码文件。在ProtoBuf编译器输出的Go语言代码文件中包含了,面向RPC客户机的远程过程调用封装类,和面向RPC服务器的服务接口。我们的工作只是在服务器侧,通过服务类实现服务接口中的服务方法,并将其实例化对象注册到RPC系统中。而在客户机侧,我们可以直接通过远程过程调用封装类的实例化对象,象调用本地方法一样调用远程方法。
下面我们创建一个名为gRPC的工程,在工程目录下创建一个子目录,命名为pb,在该目录下编辑一个ProtoBuf脚本文件arithmetic.proto:
x1syntax = "proto3";
2
3option go_package = "./;pb";
4
5message Operands {
6 int32 a = 1;
7 int32 b = 2;
8}
9
10message Result {
11 int32 c = 1;
12}
13
14service arithmetic {
15 rpc Add(Operands) returns (Result);
16 rpc Sub(Operands) returns (Result);
17}
在这段脚本中,我们先后定义了两个消息,表示运算数的Operands消息,和表示运算结果的Result消息。这和我们在上节课里,利用ProtoBuf语法描述结构化数据的做法是完全一致的。但与上节课不同的是,我们接着又定义了一个名为arithmetic的服务,该服务包含两个服务方法,Add和Sub,分别用于完成两个整数的加法和减法运算。注意这两个服务方法都带有一个Operands类型的参数用于输入运算数,和一个Result类型的返回值用于输出运算结果。
通过上节课的学习我们已经知道,扩展名为“.proto”的ProtoBuf脚本是不能被Go语言编译器编译,并与其它Go语言代码交互的。为了做到这一点需要借助ProtoBuf编译器,将其先行编译为扩展名为“.go”的Go语言代码文件。执行编译的方法就是在正确的路径下使用protoc命令,并在参数中指明Go语言代码文件的输出路径和ProtoBuf脚本文件。编译的结果是在所指定输出路径下的Go语言代码文件。与上节课的情况不同,这里的arithmetic.proto脚本中不但包含数据描述还包含服务描述,因此需要执行两次脚本编译,所使用的参数略有不同。“--go_out”参数指明编译脚本中的数据描述部分,输出arithmetic.pb.go文件,而“--go-grpc_out”参数则指明编译脚本中的服务描述部分,输出arithmetic_grpc.pb.go文件。
简单浏览一下arithmetic.pb.go文件。其中最主要的是定义了两个结构体类型,Operands和Result。很显然它们就是来自arithmetic.proto脚本中的那两个同名消息。打开arithmetic_grpc.pb.go文件,其中的内容比较多,但最主要的是ArithmeticClient接口及其实现类arithmeticClient、返回该类对象地址的NewArithmeticClient函数,以及ArithmeticServer接口和以之为参数的RegisterArithmeticServer函数。其中的arithmeticClient类即为面向RPC客户机的远程过程调用封装类,而ArithmeticServer即为面向RPC服务器的服务接口,其中的服务方法有待于我们给出具体实现。此外,我们还看到,无论是客户机侧的ArithmeticClient接口还是服务器侧的ArithmeticServer接口,均包含Add和Sub两个方法,它们显然来自arithmetic.proto脚本中对arithmetic服务的描述。
构建一个基于gRPC框架的RPC服务器,需要五个步骤。首先我们需要定义一个服务类Arithmetic,该类需要实现ArithmeticServer接口,在服务方法Add和Sub的具体实现中,完成两个整数的加法和减法运算。其次,通过grpc包的NewServer方法创建一个RPC服务器对象,将此对象和服务类Arithmetic的实例化对象一起作为参数,传递给RegisterArithmeticServer函数,完成服务注册。最后,通过net包的Listen方法,启动监听并获得监听器对象,以该对象作为参数,传递给RPC服务器对象的Serve方法,启动服务。
在gRPC工程的工程目录下,创建一个MathServer子目录,在该目录下编辑main.go文件:
xxxxxxxxxx
501package main
2
3import (
4 "context"
5 "fmt"
6 "gRPC/pb"
7 "google.golang.org/grpc"
8 "net"
9)
10
11type Arithmetic struct {
12 pb.UnimplementedArithmeticServer
13}
14
15func (arithmetic *Arithmetic) Add(
16 ctx context.Context, ops *pb.Operands) (*pb.Result, error) {
17 var res pb.Result
18 res.C = ops.A + ops.B
19 return &res, nil
20}
21
22func (arithmetic *Arithmetic) Sub(
23 ctx context.Context, ops *pb.Operands) (*pb.Result, error) {
24 var res pb.Result
25 res.C = ops.A - ops.B
26 return &res, nil
27}
28
29func main() {
30 server := grpc.NewServer()
31
32 fmt.Println("注册服务")
33 pb.RegisterArithmeticServer(server, new(Arithmetic))
34
35 fmt.Println("启动监听")
36
37 listener, err := net.Listen("tcp", "127.0.0.1:9000")
38 if err != nil {
39 fmt.Println("net.Listen错误:", err)
40 return
41 }
42
43 defer func() {
44 fmt.Println("结束监听")
45 listener.Close()
46 }()
47
48 fmt.Println("启动服务")
49 server.Serve(listener)
50}
在这段代码中,我们首先定义了一个名为Arithmetic的服务类,该类实现了ArithmeticServer接口,并在其Add和Sub方法的定义中,完成了两个整数的加法和减法运算。在main函数中,调用grpc包的NewServer方法,获得一个RPC服务器对象,将该对象连同Arithmetic对象一起,交给RegisterArithmeticServer函数,完成服务注册。之后又调用net包的Listen方法,启动监听并获得监听器对象。最终以监听器对象为参数,调用RPC服务器对象的Serve方法,启动服务。
了解了基于gRPC框架的RPC服务器,我们再来看看客户机。
构建一个基于gRPC框架的RPC客户机,需要三个步骤。首先通过grpc包的Dial方法,建立与服务器的TCP连接,并获得一个连接对象。将该连接对象作为参数,传递给NewArithmeticClient函数,创建一个与服务器相连的客户机对象。客户机对象以远程调用的方式,实现了ArithmeticClient接口中的方法。最后在用特定的数据类型定义好表示参数的变量后,调用客户机对象中的方法,同时获得返回值,象调用本地过程一样调用服务器上的远程过程。
在gRPC工程的工程目录下,创建一个MathClient子目录,在该目录下编辑main.go文件:
xxxxxxxxxx
481package main
2
3import (
4 "context"
5 "fmt"
6 "gRPC/pb"
7 "google.golang.org/grpc"
8 "google.golang.org/grpc/credentials/insecure"
9)
10
11func main() {
12 fmt.Println("请求连接")
13
14 conn, err := grpc.Dial("127.0.0.1:9000",
15 grpc.WithTransportCredentials(insecure.NewCredentials()))
16 if err != nil {
17 fmt.Println("grpc.Dial错误:", err)
18 return
19 }
20
21 defer func() {
22 fmt.Println("关闭连接")
23 conn.Close()
24 }()
25
26 arithmetic := pb.NewArithmeticClient(conn)
27
28 var ops pb.Operands
29 ops.A, ops.B = 123, 456
30
31 fmt.Println("远程调用")
32
33 if res, err := arithmetic.Add(context.TODO(), &ops); err != nil {
34 fmt.Println("ArithmeticClient.Add错误:", err)
35 return
36 } else {
37 fmt.Printf("调用返回: %d+%d = %d\n", ops.A, ops.B, res.C)
38 }
39
40 fmt.Println("远程调用")
41
42 if res, err := arithmetic.Sub(context.TODO(), &ops); err != nil {
43 fmt.Println("ArithmeticClient.Add错误:", err)
44 return
45 } else {
46 fmt.Printf("调用返回: %d-%d = %d\n", ops.A, ops.B, res.C)
47 }
48}
在这段代码中,一进入main函数即调用了grpc包的Dial方法,该方法返回一个表示与服务器连接的连接对象。以此对象为参数,调用NewArithmeticClient函数,得到客户机对象。在创建并初始化好Operands类型的参数对象后,调用客户机对象的Add和Sub方法,获得结果对象和错误对象。这两个对象均来自RPC服务器上对应方法的返回值。至此,我们不难看出,基于gRPC框架构建RPC应用,客户机与服务器间的数据传递,完全是基于ProtoBuf的序列化反序列化机制,而且对用户透明。通过ProtoBuf不但可以描述数据还可以描述服务,并据此为客户机和服务器生成完全一致的接口代码。gRPC为我们自动生成了RPC客户机与服务器的基础代码框架,实现了基于抽象接口的面向对象化的封装。
谢谢大家,我们下节课再见!