Rpc基本概念
RPC(Remote Procedure Call)远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单的理解是一个节点请求另一个节点提供的服务。RPC只是一套协议,基于这套协议规范来实现的框架都可以称为 RPC 框架,比较典型的有 有阿里巴巴的 Dubbo、Google 的 gRPC、Facebook 的 Thrift 和 Twitter 的 Finagle 等。
RPC 机制和实现过程
RPC 是远程过程调用的方式之一,涉及调用方和被调用方两个进程的交互。因为 RPC 提供类似于本地方法调用的形式,所以对于调用方来说,调用 RPC 方法和调用本地方法并没有明显区别。
RPC的机制的诞生和基础概念
1984 年,Birrell 和 Nelson 在 ACM Transactions on Computer Systems 期刊上发表了名为“Implementing remote procedure calls”的论文,该文对 RPC 的机制做了经典的诠释:
RPC 远程过程调用是指计算机 A 上的进程,调用另外一台计算机 B 上的进程的方法。其中A 上面的调用进程被挂起,而 B 上面的被调用进程开始执行对应方法,并将结果返回给 A,计算机 A 接收到返回值后,调用进程继续执行。
发起 RPC 的进程通过参数等方式将信息传送给被调用方,然后被调用方处理结束后,再通过返回值将信息传递给调用方。这一过程对于开发人员来说是透明的,开发人员一般也无须知道双方底层是如何进行消息通信和信息传递的,这样可以让业务开发人员更专注于业务开发,而非底层细节。
RPC 让程序之间的远程过程调用具有与本地调用类似的形式。比如说某个程序需要读取某个文件的数据,开发人员会在代码中执行 read 系统调用来获取数据。
当 read 实际是本地调用时,read 函数由链接器从依赖库中提取出来,接着链接器会将它链接到该程序中。虽然 read 中执行了特殊的系统调用,但它本身依然是通过将参数压入堆栈的常规方式调用的,调用方并不知道 read 函数的具体实现和行为。
当 read 实际是一个远程过程时(比如调用远程文件服务器提供的方法),调用方程序中需要引入 read 的接口定义,称为客户端存根(client-stub)。远程过程 read 的客户端存根与本地方法的 read 函数类似,都执行了本地函数调用。不同的是它底层实现上不是进行操作系统调用读取本地文件来提供数据,而是将参数打包成网络消息,并将此网络消息发送到远程服务器,交由远程服务执行对应的方法,在发送完调用请求后,客户端存根随即阻塞,直到收到服务器发回的响应消息为止。
下图展示了远程方法调用过程中的客户端和服务端各个阶段的操作。
总结下RPC执行步骤:
- 调用客户端句柄,执行传递参数。
- 调用本地系统内核发送网络消息。
- 消息传递到远程主机,就是被调用的服务端。
- 服务端句柄得到消息并解析消息。
- 服务端执行被调用方法,并将执行完毕的结果返回给服务器句柄。
- 服务器句柄返回结果,并调用远程系统内核。
- 消息经过网络传递给客户端。
- 客户端接受数据。
安装gRPC和Protobuf
gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
gRPC客户端和服务端可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言写客户端调用
在gRPC中,我们可以一次性的在一个 proto文件中定义服务并使用任意的支持gRPC的语言去实现客户端和服务端,整个过程操作变得简单,就像调用本地函数一样。
通过 proto生成服务端代码,也就是服务端的骨架,提供低层通信抽象
通过 proto生成客户端代码,也就是客户端的存根,隐藏了不同语言的差异,提供抽象的通信方式,就像调用本地函数一样。
安装
- go get github.com/golang/protobuf/proto
- go get google.golang.org/grpc(无法使用,用如下命令代替)
- git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
- git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
- git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
- go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
- git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
- cd $GOPATH/src/
- go install google.golang.org/grpc
- go get github.com/golang/protobuf/protoc-gen-go
- 上面安装好后,会在GOPATH/bin下生成protoc-gen-go.exe
- 但还需要一个protoc.exe,windows平台编译受限,很难自己手动编译,直接去网站下载一个,地址:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.0 ,同样放在GOPATH/bin下
proto 服务定义
gRPC 使用protocol buffer 来定义服务接口,protocol buffer和 XML、JSON一样是一种结构化数据序列化的可扩展存储结构,protocol buffer是一种语言中立,结构简单高效,比XML更小更简单,可以通过特殊的插件自动生成代码来读写操作这个数据结构。
import "myproject/other_protos.proto"; // 导入其他 proto文件 syntax = "proto3"; // 指定proto版本 package hello; // 指定默认包名 // 指定golang包名 option go_package = "hello"; message SearchRequest { required string query = 1; // 必须赋值字段 optional int32 page_number = 2 [default = 10]; // 可选字段 repeated int32 result_per_page = 3; // 可重复字段 } message SearchResponse { message Result // 嵌套定义 { required string url = 1; optional string title = 2; repeated string snippets = 3; } repeated Result result = 1; } message SomeOtherMessage { optional SearchResponse.Result result = 1; // 使用其他消息的定义 } service List{ // 定义gRPC服务接口 rpc getList(SearchRequest) returns (SearchResponse); }
// 插件自动生成gRPC骨架和存根 protoc --go_out=plugins=grpc: ./ *.proto
后面需要实现服务端具体的逻辑就行,然后注册到gRPC服务器
客户端在调用远程方法时会使用阻塞式存根,所以gRPC主要使用同步的方式通信,在建立连接后,可以使用流的方式操作。
客户端编排为protocol buffer的格式,服务端再解排执行,以HTTP2 传输
gRPC 优势
- 更高效的进程通信:使用基于protocol buffer在Http2 中以二进制协议通信,而不是JSON、XML文本格式
- 简单定义的服务接口、易扩展
- 强类型、跨语言
- 一元RPC、服务端流、客户端流、双工流
gRPC入门
简单使用
protocol buffer
syntax = "proto3"; package hello; // 第一个分割参数,输出路径;第二个设置生成类的包路径 option go_package = "./proto/hello"; // 设置服务名称 service Greeter { // 设置方法 rpc SayHello (HelloRequest) returns (HelloReply) {} } // 请求信息用户名. message HelloRequest { string name = 1; } // 响应信息 message HelloReply { string message = 1; }
服务端
package main import ( "context" "flag" "fmt" "log" "net" "google.golang.org/grpc" pb "mygrpc/proto/hello" ) var ( port = flag.Int("port", 50051, "The server port") ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } // 开启rpc s := grpc.NewServer() // 注册服务 pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
客户端
package main import ( "context" "flag" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "mygrpc/proto/hello" // 引入编译生成的包 ) const ( defaultName = "world" ) var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") name = flag.String("name", defaultName, "Name to greet") ) func main() { flag.Parse() // 与服务建立连接. conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() // 创建指定服务的客户端 c := pb.NewGreeterClient(conn) // 连接服务器并打印出其响应。 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.GetMessage()) }
客户端连接gRPC服务器以后,就可以像调用本地函数一样操作远程服务器。
代码仓库
https://github.com/onenewcode/mygrpc.git
也可以直接下载绑定的资源。