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的过程,让开发者调用远程方法就像调用本地方法一样。
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进行数据传输。
下面我们使用代码基于以上的步骤来实现一个简单gRPC。我们用Go实现server端,Java作为client端来实现。
下载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;
}
根据官方文档使用如下命令安装针对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 main
import (
"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")
}
}
目前服务端已经完成了。
package main
import (
"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
实现Java作为客户端调用go服务端的服务