1什么是RPC
RPC:Remote Procedure Call,远程过程调用。简单来说就是两个进程之间的数据交互。正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者自身调用的,也就是本地过程调用。和本地过程调用相对的就是:假如两个服务端不在一个进程内怎么进行数据交互?使用RPC。尤其是现在微服务的大量实践,服务与服务之间的调用不可避免,RPC更显得尤为重要
上图描述了一个RPC的完整调用流程:
1:client向client stub发起方法调用请求。
2:client stub接收到请求后,将方法名,请求参数等信息进行编码序列化。
3:client stub通过配置的ip和端口使用socket通过网络向远程服务器server发起请求。
4:远程服务器server接收到请求,解码反序列化请求信息。
5:server将请求信息交给server stub,server stub找到对应的本地真实方法实现。
6:本地方法处理调用请求并将返回的数据交给server stub。
7:server stub 将数据编码序列化交给操作系统内核,使用socket将数据返回。
8:client端socket接收到远程服务器的返回信息。
9:client stub将信息进行解码反序列化。
10:client收到远程服务器返回的信息。
上图中有一个stub(存根)的概念。stub负责接收本地方法调用,并将它们委托给各自的具体实现对象。server端stub又被称为skeleton(骨架)。可以理解为代理类。而实际上基于Java的RPC框架stub基本上也都是使用动态代理。我们所说的client端和server端在RPC中一般也都是相对的概念。
而所谓的RPC框架也就是封装了上述流程中2-9的过程,让开发者调用远程方法就像调用本地方法一样。
2. gRPC的原理
gRPC是Google的开源产品,是跨语言的通用型RPC框架,使用Go语言编写。 Java语言的应用同样使用了Netty做网络通信,Go采用了Goroutine做网络通信。序列化方式采用了Google自己开源的Protobuf。请求的调用和返回使用HTTP2的Stream。
一个RPC框架必须有两个基础的组成部分:数据的序列化和进程数据通信的交互方式。
对于序列化gRPC采用了自家公司开源的Protobuf。Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。似乎和我们熟悉的JSON类似,但其实着重点有些本质的区别。JSON主要是用于数据的传输,因为它轻量级,可读性好,解析简单。Protobuf主要是用于跨语言的IDL,它除了和JSON、XML一样能定义结构体之外,还可以使用自描述格式定于出接口的特性,并可以使用针对不同语言的protocol编译器产生不同语言的stub类。所以天然的适用于跨语言的RPC框架中(非常重要)
而关于进程间的通讯,无疑是Socket。Java方面gRPC同样使用了成熟的开源框架Netty。使用Netty Channel作为数据通道。传输协议使用了HTTP2。
通过以上的分析,我们可以将一个完整的gRPC流程总结为以下几步:
● 通过.proto文件定义传输的接口和消息体。
● 通过protocol编译器生成server端和client端的stub程序。
● 将请求封装成HTTP2的Stream。
● 通过Channel作为数据通信通道使用Socket进行数据传输。
3 实践开始
下面我们使用代码基于以上的步骤来实现一个简单gRPC。我们用Go实现server端,Java作为client端来实现。
3.1 安装Protocol Buffers,定义.proto文件
下载Protocol Buffers:https://github.com/protocolbuffers/protobuf/releases
检查安装
protoc --version
定义一个simple.proto,这也是后续实现gRPC的基础:
syntax = "proto3"; //定义了我们使用的Protocol Buffers版本。option go_package = "./;simple";//***在java端请注释本行***//表明我们定义了一个命名为Simple的服务(接口),内部有一个远程rpc方法,名字为SayHello。//我们只要在server端实现这个接口,在实现类中书写我们的业务代码。在client端调用这个接口。service Simple{rpc SayHello(HelloRequest) returns (HelloReplay){}}//请求的结构体message HelloRequest{string name = 1;}//返回的结构体message HelloReplay{string message = 1;}
3.2 在Go端实现server
根据官方文档使用如下命令安装针对Go的gRPC:
go get -u google.golang.org/grpc
建立Go的project:go-server-grpc,然后将前面写的simple.proto放入项目proto的package中。
cd到proto目录执行如下命令:
protoc --go_out=plugins=grpc:. simple.proto
这样就将simple.proto编译成了Go语言对应的stub程序了。
随后我们就可以写我们server端的代码了:main.go。
以下的代码都是模板代码,main函数是socket使用Go的标准实现。作为开发者我们只关注远程服务提供的具体接口实现即可。
我们可以在生成的simple.pb.go中发现需要实现的接口:
// 客户端调用的接口
type SimpleClient interface {SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error)
}type simpleClient struct {cc grpc.ClientConnInterface
}func NewSimpleClient(cc grpc.ClientConnInterface) SimpleClient {return &simpleClient{cc}
}func (c *simpleClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error) {out := new(HelloReplay)err := c.cc.Invoke(ctx, "/Simple/SayHello", in, out, opts...)if err != nil {return nil, err}return out, nil
}// 服务端需要实现的接口
type SimpleServer interface {SayHello(context.Context, *HelloRequest) (*HelloReplay, error)
}
之后我们在main.go中实现接口:
package mainimport ("context""grpc-server/proto"//引入对应的包"fmt""net""log""google.golang.org/grpc""google.golang.org/grpc/reflection"
)
//定义接下来要开放的socket的端口
const(port = ":50051"
)type server struct{}func (s *server) SayHello(ctx context.Context,req *simple.HelloRequest) (*simple.HelloReplay, error){fmt.Println(req.Name)return &simple.HelloReplay{Message:"hello =======> " + req.Name},nil
}
之后填写main方法:
func main() {//创建一个socket监听lis, err := net.Listen("tcp", port)if err != nil {log.Fatal("fail to listen")}//新建一个grpc服务器s := grpc.NewServer()//使用 simple.RegisterSimpleServer 函数将实现了 SimpleServer 接口的 server 对象注册到 gRPC 服务器(s)上simple.RegisterSimpleServer(s, &server{})//使用 reflection.Register 函数将 gRPC 服务器(s)注册到反射服务中。这样,可以通过 gRPC 提供的工具来动态地查看和调用服务器上的服务。reflection.Register(s)//使用 s.Serve 方法启动 gRPC 服务器(s),开始接受来自客户端的连接请求并提供服务。如果启动过程中出现错误,程序会输出一条错误信息并终止运行。if err := s.Serve(lis); err != nil {log.Fatal("fail to server")}
}
目前服务端已经完成了。
3.3 在Go端实现client
package mainimport ("context""fmt"simple "go-server-grpc/proto""log""google.golang.org/grpc"
)const (address = "localhost:50051"
)func main() {// 创建与服务器的连接conn, err := grpc.Dial(address, grpc.WithInsecure())if err != nil {log.Fatalf("无法连接到服务器:%v", err)}defer conn.Close()// 创建一个新的 gRPC 客户端client := simple.NewSimpleClient(conn)// 构建请求request := &simple.HelloRequest{Name: "John",}// 调用 gRPC 方法response, err := client.SayHello(context.Background(), request)if err != nil {log.Fatalf("调用 gRPC 方法失败:%v", err)}// 打印响应fmt.Println(response.Message)
}
需要先启动服务端,再启动客户端就可以看到效果。
输出:hello=======>John
4.下次预告
实现Java作为客户端调用go服务端的服务