何为 RPC
RPC(Remote Procedure Call),远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC 协议假定某些传输协议的存在,如 TCP 或 UDP,为通信程序之间携带信息数据。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。
gRPC
gRPC 是由 Google 牵头开发,语言中立、平台中立、开源的远程过程调用系统。其遵循一般的 RPC 系统的特点设计:即定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
安装与测试运行
开始之前…
在安装和使用 gRPC 之前,我们首先需要安装 Protocol Buffers,它是Google 开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。gRPC 依靠 Protocol Buffers 来定义服务。
安装
通过如下命令即可安装 Protocol Buffers 和 gRPC:(由于引用了 Google 的 Golang 库,您可能需要代理运行)
1 | go get -u github.com/golang/protobuf/protoc-gen-go |
代理运行的方式:
1 | http_proxy=http://address:port go get ... |
运行样例
在我们刚才安装时获取的文件中,gRPC 为我们附带了其的运行样例,这个样例存储于 $GOPATH/src/google.golang.org/grpc/examples
,下面我们以其中的 helloworld
为例进行讲解:
编译和运行
首先,我们进入样例工作目录。注意,请确保您已经正确配置了 GOPATH
环境变量。
1 | cd $GOPATH/src/google.golang.org/grpc/examples/helloworld |
在样例中,我们的文件都已经被正确编译为了 .pb.go
文件,实际上,这个文件是由服务定义文件 .proto
编译而来的。如果您想要自己编译,请使用我们通过 protoc-gen-go
引入的 protoc 编译器(请确保您的环境变量包含 \$GOPATH/bin
)。
接下来,我们编译并运行生成的服务端和客户端 go 文件:
1 | # Run on terminal 1: Server side |
您会看到在客户端的终端界面中出现了 Greeting: Hello world
,这表示我们的安装配置已经成功。
源码分析
gRPC 是如何实现通过 go 语言满足我们在 proto 中定义的服务呢,我们可以通过分析生成的 go 语言代码来初步了解:
虽然我们通过 greeter_server/main.go
和 greeter_client/main.go
启动的 gRPC 服务,但这个服务真正的核心是他们共同引用的 helloworld/helloworld.pb.go
。
首先我们看看 .proto
文件都写了些什么:
1 | // 指定 Protocol Buffer 的语法版本 |
然后,我们抛开冗余的生成代码不看,helloworld.pb.go
实际上做了这几件事:
定义 HelloRequest 数据结构
1
2
3
4
5
6type 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:"-"`
}其中,
Name
是我们在.proto
中定义的字段。定义 HelloReply 数据结构
1
2
3
4
5
6type 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:"-"`
}其中,
Message
是我们在.proto
中定义的字段。定义数据结构操作方法
1
2
3
4
5
6
7
8func (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
}除去一些基本的生成方法,这个方法定义了我们在
proto
中定义的方法,它允许客户端调用这个方法来向服务端发送一个 HelloRequest,并接受一个 HelloReply。
然后,我们就可以看到这些内容在服务端和客户端的使用了:
服务端:
服务端的 go 语言代码首先实现了我们在类文件中没有实现的定义:
1
2
3
4func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.Name)
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}这样在其收到一个 HelloRequest 的时候就会调用这个方法来发送一个 HelloReply 给客户端。
然后,其监听给定端口,创建服务器对象,注册服务器并且将其绑定到端口上:
1
2
3
4
5lis, err := net.Listen("tcp", port)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
err := s.Serve(lis)客户端:
客户端首先尝试创建一个到服务端的连接,然后创建一个客户端对象,调用 SayHello 方法发送一个 HelloRequest 并回收一个 HelloReply:
1
2
3
4
5conn, err := grpc.Dial(address, grpc.WithInsecure())
c := pb.NewGreeterClient(conn)
name := defaultName
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
至此,运行两个 main.go
即可达到我们现前使用的效果,gRPC 服务也可正常运行。