最近,服务化和微服务化逐渐成为中大型分布式系统的主流方式,而RPC在其中也扮演着至关重要的角色。这里,我们就简单介绍一下什么是RPC,以及通过gRPC的一个简单的例子,来看看如何通过gRPC进行开发。
1. 什么是RPC
RPC(Remote Procedure Call),即远程程序调用,是进程间通信的一种方式。区别于本地调用(Local Call)中调用者调用同一个地址空间上的函数,RPC允许程序调用另一个地址空间的(通常是共享网络的另一台机器上)的函数,而不用程序员显式编码这个远程调用的细节,使得在程序员的角度看来,远程调用和本地调用具有一样的效果。
RPC这个概念很早就已经出现了,在Bruce Jay Nelson在1984年的论文Implementing RPC中就给出了一个具体的实现。同时在这片论文中,Bruce给出了RPC的三个好处:
- 简单与简洁的语法:这使得构建分布式系统变得更加容易,更加准确;
- 高效:通过RPC进行函数调用足够简单,使得通信更加迅速;
- 通用:在单机系统中过程往往是不同算法部分最重要的通信机制。
其实简单说,从调用者的角度来看,RPC和普通的本地调用没有什么区别,都是调用别的函数的过程;但在实现的角度来看,RPC就是通过网络在不同的机器之间进行通信,完成普通调用在同一个地址空间就可以完成的参数传递以及结果回传。Bruce的文章发表在1984年,足见他的观点的高瞻远瞩,我们今天使用的RPC框架基本就是按照这个设计实现的。
从上面的介绍中我们可以看出,RPC和普通本地调用不同的地方在于参数和结果的传递方式。RPC是通过网络进行传递的,因此,对于一个RPC系统来说就需要仔细思考其中的实现细节。这里我们不过多进行涉及,仅仅了解概念就好。
在论文中,Bruce指出实现一个RPC系统需要如下的几个部分:
- User;
- User-Stub;
- RPCRuntime;
- Server-Stub;
- Server。
这五个部分的具体结构如图所示:
其中User、User-Stub和一个RPCRuntime实例运行在调用者(caller)机器上,而Server、Server-Stub和另一个RPCRuntime实例运行在被调用者(callee)机器上。
具体的调用过程如下:
当一个User想发起一个远程调用的时候,它其实首先先本地调用User-Stub中的相关程序,而User-Stub负责得到这次调用具体的远程程序是什么以及将调用的参数传递给caller端的RPCRuntime,RPCRuntime会将参数通过网络传递给目标机器的RPCRuntime。而目标机器上的RPCRuntime收到这个请求后把参数传递给Server-Stub,之后Server-Stub解析参数并发起一个普通的本地调用,使得Server进行执行。当Server执行完之后,将结果返回给Server-Stub,然后再通过网络回传给caller。caller端的User-Stub解析结果后将结果返回给User。
现在的RPC框架基本都支持不同的语言,也就是说User和Server可以是不同的语言实现的程序,那么就需要RPC框架在中间进行一个接口的定义与统一。这里就是使用了IDL(Interface Definition Language)来定义接口的,然后通过框架提供的工具来分别对应User和Server生成相应语言的Stub。
2. gRPC登场
gRPC是Google开源的一个RPC框架,它使用protocol buffers作为IDL以及底层消息转换格式。就像上面介绍的一样,gRPC的结构如图:
不多说,我们通过一个简单的例子看看如何使用gRPC。
3. HelloWorld in gRPC
为了使用gRPC,我们需要Go 1.6或更高的版本:
$ go version
如果没有安装Go的话,可以参考这个。安装完Go后,需要设置好GOPATH。
接下来,我们需要安装gRPC。我们可以使用下面的命令进行安装:
$ go get -u google.golang.org/grpc
不过这样安装需要科学上网。如果不能科学上网的话,也可以通过github.com
来安装。
首先进入第一个$GOPATH目录,go get
默认安装在第一GOPATH下,新建google.golang.org
目录,拉取golang
在github
上的镜像库:
$ cd /User/valineliu/go/src
$ mkdir google.golang.org
$ cd google.golang.org/
$ git clone https://github.com/grpc/grpc-go
$ mv grpc-go/ grpc
然后再安装Protobuf Buffers v3。protobuf buffers作为gRPC的IDL和底层消息转换的工具,我们需要安装对应的protoc编译器。
首先在这里下载相关版本的文件:
$ wget https://github.com/google/protobuf/releases/download//protobuf-all-.zip
$ unzip protobuf-all-.zip
$ cd protobuf-all-
$ ./configure
$ make
$ make install
这样就完成了protoc的安装。
接下来安装protoc的Go插件:
$ go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go是protoc对应Go的一个插件,用来根据IDL描述文件生成Go语言的代码。上面的命令会将其安装在$GOPATH/bin目录下,然后将其添加到PATH中:
$ export PATH=$PATH:$GOPATH/bin
这样所有的组件都安装完毕了,接下来开始我们的HelloWorld
。
项目的目录结构如下:
GOPATH
|__src
|__helloworld
|__client
|__helloworld
|__server
...
其中client
目录用来存放Client端的代码,helloworld
目录用来存放服务的IDL定义,server
目录用来存放Server端的代码。
首先,我们需要定义我们的服务。进入helloworld
目录,新建文件:
$ cd helloworld
$ vim helloworld.proto
这里我是使用vim进行编辑的。定义如下:
syntax = "proto3";
package helloworld;
message HelloRequest {
string name=1;
}
message HelloReply {
string message =1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
这里我们先不看具体的含义。然后,我们使用protoc
对这个文件进行编译:
$ protoc -I. --go_out=plugins=grpc:. helloworld.proto
这样,目录下多了一个文件:helloworld.pb.go
。
然后进入server
目录,新建文件:
$ cd ../server
$ vim server.go
server.go
的内容如下:
package main
import (
"log"
"net"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "helloworld/helloworld"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
type server struct{}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v",err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v",err)
}
}
这就是Server端的代码,同样,我们暂时不考虑具体的细节。
然后进入client
目录新建文件:
$ cd ../client
$ vim client.go
client.go
的内容如下:
package main
import (
"log"
"os"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "helloworld/helloworld"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v",err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name:name})
if err!=nil {
log.Fatalf("could not greet: %v",err)
}
log.Printf("Greeting: %s",r.Message)
}
这就是Client端的代码。
这样所有的代码就编写完了,然后我们就要让它跑起来。
首先进入server
目录,启动我们的服务器:
$ cd ../server
$ go run server.go
如图所示,服务器启动起来了:
然后另外开启一个终端,进入client
目录,发起一个RPG远程调用:
$ cd $GOPATH/src/helloworld/client
$ go run client.go // one
$ go run client.go firework //two
结果如图:
成功!我们的第一个小例子完成了,这篇先到这,To Be Continued~
4. 系列目录
- Dive into gRPC(1):gRPC简介
- Dive into gRPC(2):实现一个服务
- Dive into gRPC(3):安全通信
- Dive into gRPC(4):Streaming
- Dive into gRPC(5):验证客户端
- Dive into gRPC(6):metadata