浅谈 rpc 与 grpc

什么是 RPC

分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用远程方法调用,例:Java RMI

PRC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,PRC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理进行交互。许多技术(常常是不兼容)都是基于这种概念而实现的。

​ ------- from wikipedia

简单来说,rpc 就是在本地像调用本地函数一样调用远程服务上的函数,举个栗子(grpc举例):

远程 server 上定义函数

package main

import (
	"context"
	pb "github.com/dalebao/grpc_demo/proto"
	"google.golang.org/grpc"
	"log"
	"net"
)

type SearchService struct {
}

//定义Search函数
func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {
	return &pb.SearchResponse{Response: r.GetRequest() + " Server"}, nil
}


const PORT = "9001"

func main() {
	server := grpc.NewServer()
  //注册
	pb.RegisterSearchServiceServer(server, &SearchService{})

	lis, err := net.Listen("tcp", ":"+PORT)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}

	server.Serve(lis)
}

client 端调用:

package main

import (
	"context"
	"log"

	"google.golang.org/grpc"
	//定义的grpc protocol
	pb "github.com/dalebao/grpc_demo/proto"
)

const PORT = "9001"

func main() {
	conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("grpc.Dial err: %v", err)
	}
	defer conn.Close()

	client := pb.NewSearchServiceClient(conn)
  //调用远程Search函数
	resp, err := client.Search(context.Background(), &pb.SearchRequest{
		Request: "gRPC1",
	})
	if err != nil {
		log.Fatalf("client.Search err: %v", err)
	}

	log.Printf("resp: %s", resp.GetResponse())
}

简单感受一下 grpc 的调用过程。

RPC 与 HTTP

  1. 传输协议

    • rpc : 可以基于 tcp 也可以基于 http
    • http: 基于 http 协议
  2. 传输效率

    • rpc : 可以使用自定义的 tcp 协议,也可以使用 http2 协议,减小报文的体积。
    • http:基于 http 1.1,请求中会包含很多无用信息,使用 http2 协议,可以当成是一种 rpc
  3. 性能消耗

    • rpc:可以基于thrift实现高效的二进制传输
    • http: 大部分是通过json来实现的,字节大小和序列化耗时都比thrift 要更消耗性能
  4. 负载均衡

    • rpc: 基本自带
    • http: 需要nginx等工具实现
  5. 服务治理

    • rpc: 能做到自动通知,不影响上游
    • http: 需要事先通知,修改nginx 配置

总结: rpc 主要用于系统内部的相互调用,拥有传输效率高,性能高等优点。http 协议用于对外的api 构建。

RPC 与 HTTP Api 调用的流程区别

在定义好之后 client 与 server 之间的沟通方式(如json)之后,使用 rpc 与 普通 http api 在调用方的处理逻辑有比较大区别。 rpc 可以更好的关注业务逻辑。

例如,在不考虑请求失败的情况下,使用 http api 沟通的 client 拿到服务器返回的数据需要进行以下操作:

st=>start: 获取返回json数据
e=>end: 获取正确,开始业务逻辑处理
op1=>operation: 解析json串
op1_c=>condition: 解析是否正确
op2_c=>condition: 数据是否正确
sub1=>subroutine: My Subroutine
para=>parallel: 抛出异常

st->op1->op1_c
op1_c(yes)->op2_c
op1_c(no)->para
op2_c(yes)->e
op2_c(no)->para

也就是说,在获取 server 返回的数据之后,我们需要判断数据是否是预先定义的沟通方式。在判断数据不同的时候,需要对不同的 api 做很多适配。

同样在请求 server 的过程中,也需要按照 http 协议的模式,填写 header 等,而且存在不同请求方式的区别,在代码中体现很多业务不相关的细节。

但是,rpc 的调用不放不需要在代码中考虑这些细节。我们会预先设计好 server 与 client 沟通的数据结构,在调用的过程中,client只要传输给server预定好的数据结构,那么也将会获得对应的数据。

GRPC

gRpc 是谷歌开源的一个通用 rpc 框架,具有以下特性:

  • 强大的IDL,使用Protocol Buffers作为数据交换的格式,支持v2v3(推荐v3

  • 跨语言、跨平台,也就是gRpc支持多种平台和语言

  • 支持 HTTP2 ,双向传输、多路复用、认证等

IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。

简单来讲 gRPC 是一个 基于 HTTP/2 协议设计的 RPC 框架,它采用了 Protobuf 作为 IDL

安装gRpc

$ go get -u google.golang.org/grpc

常见的 RPC 框架对比

\ 跨语言 多 IDL 服务治理 注册中心 服务管理
gRpc × × × ×
Thrift × × × ×
Rpcx ×
Dubbo ×

Protobuf

Protocol Buffers 是一种与语言、平台无关,可扩展的序列化结构化数据的方法,常用于通信协议,数据存储等等。相较于 JSON、XML,它更小、更快、更简单,因此也更受开发人员的青眯。

语法如下:

search.proto

syntax = "proto3";

package proto;
//定义服务结构体
service SearchService {
    rpc Search(SearchRequest) returns (SearchResponse) {}
}

//定义请求结构体
message SearchRequest {
    string request = 1;
}

//定义响应结构体
message SearchResponse {
    string response = 1;
}

Protobuf 编译器会根据选择的语言不同,生成相应语言的 Service Interface Code 和 Stubs。

详细的语法

Protocol Buffers v3 安装

wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip
unzip protobuf-all-3.5.1.zip
cd protobuf-3.5.1/
./configure
make
make install

Protocol plugin

go get -u github.com/golang/protobuf/protoc-gen-go

编译成go语言

search.proto文件目录下执行:

protoc --go_out=plugins=grpc:. *.proto
  • 涉及到rpc服务,在plugins 后面指定grpc

会得到一个*.pb.go 文件,内容为一个符合grpc 规范的 go 代码。

gRpc调用过程概览

  1. 客户端(gRPC Sub)调用 A 方法,发起 RPC 调用
  2. 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)
  3. 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回
  4. 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)
  5. 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果

gRPC 流的三种类型

  • Server-side streaming RPC:服务器端流式 RPC

  • Client-side streaming RPC:客户端流式 RPC

  • Bidirectional streaming RPC:双向流式 RPC

流的存在是为了解决大规模数据包与实时场景的需求。

  1. 服务端流式 RPC

    客户端发起一次 rpc 请求,服务端发送多次响应

  2. 客户端流式 RPC

    客户端发起多次rpc 请求,服务端响应一次

  3. 双向流式 RPC

    由客户端以流式的方式发起请求,服务端同样以流式的方式响应请求

    首个请求一定是 Client 发起,但具体交互方式(谁先谁后、一次发多少、响应多少、什么时候关闭)根据程序编写的方式来确定(可以结合协程)

实现看代码

安全

  1. 为了避免 grpc 请求过程中的数据裸奔在网络中,rpc可以使用 TLS 加密。

  2. 如果希望该 grpc 服务,获得认证请求的能力,可以对服务做自定义的认证。

实现看代码

总结

grpc 拥有 调用方便、传输速度快等优点。同时,由于 grpc 使用 http2.0 协议,意味着,我们可以对数据进行监听,分析协议,就可以实现让服务同时提供 http api 服务。

grpc 的使用场景丰富,可以是普通的接口调用,也可以是大量数据的流传输。跨语言的特性,可以快速的让各种语言对接进服务中来。

但,grpc 不适合用于对外的服务使用。比较适合在内部系统中的项目之间使用。

http 1.1 与 http2

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
  • header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
  • 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。

区别