RPC
Remote Procedure Call,远程过程调用,是用来屏蔽分布式计算中的各种调用细节,使得调用远端的方法就像调用本地的一样。
客户端与服务端沟通的过程
- 客户端发送数据(以字节流的方式);(编码)
- 服务端接受并解析,根据约定知道要执行什么,然后把结果返回给客户。(解码)
RPC:
- RPC就是将上述过程封装下,使其操作更加优化;
- 使用一些大家都认可的协议,使其规范化;
- 做成一些框架,直接或间接产生利益。
RPC 和 HTTP 区别
RPC 和 HTTP 不是对等的概念。
- RPC 是一个完整的远程调用方案,它包括了:接口规范 + 序列化反序列化规范 + 通信协议等。
- HTTP 只是一个通信协议,工作在 OSI 的第七层,不是一个完整的远程调用方案。
RPC是能够基于 HTTP 实现,也可以不基于,基于更下一层的 TCP/ UDP协议。
RPC 原理
- 调用方为
client
,被调用方为server
。
① 服务调用方(client)以本地调用方式调用服务;
② client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
③ client stub 找到服务地址,并将消息发送到服务端;
④ server 端接收到消息;
⑤ server stub 收到消息后进行解码;
⑥ server stub 根据解码结果调用本地的服务;
⑦ 本地服务执行并将结果返回给 server stub;
⑧ server stub 将返回结果打包成能够进行网络传输的消息体;
⑨ 按地址将消息发送至调用方;
⑩ client 端接收到消息;
⑪ client stub 收到消息并进行解码;
⑫ 调用方得到最终结果。
RPC 接口和传统的 http 接口的区别
传输协议
- RPC:可以基于TCP协议,也可以基于HTTP协议。
- HTTP:基于HTTP协议。
传输效率
- RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率。
- HTTP:如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理。
服务治理
- RPC:能做到自动通知,不影响上游。
- HTTP:需要事先通知,修改Nginx/HAProxy配置。
GRPC
gRPC 是一个高性能的、开源的、通用的 RPC 框架。
- grpc 只是一个框架,是对rpc的封装,类似的框架还有thrift等。
- 目标是只需要关心第1步和最后1步,中间的其他步骤统统封装起来,让使用者无需关心。
grpc 解决的 rpc 三大问题
协议约定
gRPC 的协议是 Protocol Buffers,是一种压缩率极高的序列化协议,Google 在 2008 年开源了 Protocol Buffers,支持多种编程语言,所以 gRPC 支持客户端与服务端可以用不同语言实现。
传输协议
gRPC 的数据传输用的是 Netty Channel, Netty 是一个高效的基于异步 IO 的网络传输架构。Netty Channel 中,每个 gRPC 请求封装成 HTTP 2.0 的 Stream。
服务发现
gRPC 本身没有提供服务发现的机制,需要通过其他组件。
Protocol Buffs
Protocol Buffss 是谷歌开源的一套成熟的数据结构序列化机制。
- 序列化:将数据结构或对象转换成二进制串的过程。
- 反序列化:将在序列化过程中所产生的二进制串转换成数据结构或者对象的过程。
protobuf 是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
优势
- 序列化后体积相比Json和XML很小,适合网络传输
- 支持跨平台多语畜
- 消息格式升级和兼容性还不错
- 序列化反序列化速度很快
grpc-study
proto文件编写
hello.proto
:
// 这是说明使用的是proto3语法
syntax = "proto3";
// 这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中
// . 代表在当前目录生成
// service 代表了生成的go文件的包名是service
option go_package = ".;service";
// 然后需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应
// 定义了一个service,称为SayHello,这个服务中有一个rpc方法,名为SayHello
// 这个方法会发送一个HelloRequest,然后返回一个HelloResponse
service SayHello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
// message关键字,可以理解为Golang中的结构体
// 这里比较特别的是变量后面的“赋值”。注意,这里并不是赋值,而是在定义这个变量在这个message中的位置。
message HelloRequest {
string requestName = 1;
// int64 age = 2;
}
message HelloResponse {
string responseMsg = 1;
}
执行命令
protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto
生成 hello_grpc.pb.go
文件和 hello.pb.go
文件。
proto文件介绍
message
- protobuf 中定义一个消息类型式是通过关键字 message 字段指定的。
- 消息就是需要传输的数据格式的定义。
- message 关键字类似于 C++ 中的 class,JAVA 中的 class,go 中的 struct。
- 在消息中承载的数据分别对应于每一个字段,其中每个字段都有一个名字和一种类型。
- 一个 proto 文件中可以定义多个消息类型。
hello.proto
:
// message关键字,可以理解为Golang中的结构体
// 这里比较特别的是变量后面的“赋值”。
// 注意,这里并不是赋值,而是在定义这个变量在这个message中的位置。
message HelloRequest {
string requestName = 1;
// int64 age = 2;
}
message HelloResponse {
string responseMsg = 1;
}
字段规则
- required:消息体中必填字段,不设置会导致编码异常。在 protobuf2 中使用,在 protobuf3 中被删去。
- optional:消息体中可选字段。protobuf3 没有了required,optional 等说明关键字,都默认为optional 。
- repeate:消息体中可重复字段,重复的值的顺序会被保留在go中重复的会被定义为切片。
hello.proto
:
message HelloRequest {
string requestName = 1;
int64 age = 2;
repeated string name = 3;
}
执行 protoc --go_out=. hello.proto
hello.pb.go
:
type HelloRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestName string `protobuf:"bytes,1,opt,name=requestName,proto3" json:"requestName,omitempty"`
Age int64 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Name []string `protobuf:"bytes,3,rep,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
消息号
在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是 [1,2^29-1] 范围内的一个整数。
嵌套消息
可以在其他消息类型中定义、使用消息类型。
// Person 消息定义在 PersonInfo 消息内
message PersonInfo {
message Person{
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
如果要在它的父消息类型的外部重用这个消息类型,需要Personlnfo.Person的形式使用它。
message PersonMessage {
message Person{
PersonInfo.Person info = 1;
}
服务定义
如果想要将消息类型用在 RPC 系统中,可以在 .proto 文件中定义一个 RPC 服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。
service searchservicef
# rpc 服务函数名 (参数) 返回 (返回参数)
rpc search(searchRequest) returns (searchResponse)
}
定义了一个RPC服务,该方法接受 SearchRequest 返回 SearchResponse 。
服务端编写
- 创建 gRPC Server 对象,可以理解为它是 Server 端的抽象对象。
- 将 server (其包含需要被调用的服务端接口) 注册到 gRPC Server 的内部注册中心。
这样可以在接受到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理。 - 创建 Listen,监听 TCP 端口。
- gRPC Server开始 lis.Accept,直到 Stop
hello-server\main.go
:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
pb "grpc-study/hello-server/proto"
"net"
)
// hello server
type server struct {
pb.UnimplementedSayHelloServer
}
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello " + req.RequestName}, nil
}
func main() {
// 开启端口
listen, _ := net.Listen("tcp", ":9090")
// 创建grpc服务
grpcServer := grpc.NewServer()
// 在grpc服务端中去注册我们自己编写的服务
pb.RegisterSayHelloServer(grpcServer, &server{})
// 启动服务
err := grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
}
}
客户端编写
- 创建与给定目标 (服务端) 的连接交互。
- 创建 server的客户端对象。
- 发送 RPC 请求,等待同步响应,得到回调后返回响应结果。
- 输出响应结果。
hello-clinent\main.go
:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "grpc-study/hello-server/proto"
"log"
)
func main() {
// 连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 建立连接
clinent := pb.NewSayHelloClient(conn)
// 执行rpc调用(这个方法在服务器端来实现并返回结果)
resp, _ := clinent.SayHello(context.Background(), &pb.HelloRequest{RequestName: "Shigy"})
fmt.Println(resp.GetResponseMsg())
}
运行结果
先运行服务端再运行客户端:
认证-安全传输
gRPC 是一个典型的 C/S 模型,需要开发客户端和服务端,客户端与服务端需要达成协议,使用某一个确认的传输协议来传输数据,gRPC 通常默认是使用 protobuf 来作为传输协议,当然也是可以使用其他自定义的。
客户端与服务端要通信之前,客户端如何知道自己的数据是发给哪一个明确的服务端呢?反过来,服务端是不是也需要有一种方式来弄个清楚自己的数据要返回给谁呢?
那么就不得不提 gRPC 的认证。
此处说到的认证,不是用户的身份认证,而是指多个 server 和多个 client 之间,如何识别对方是谁,并且可以安全的进行数据传输。
- SSL/TLS 认证方式 (采用 http2 协议)
- 基于 Token 的认证方式 (基于安全连接)
- 不采用任何措施的连接,这是不安全的连接 (默认采用 http1)
- 自定义的身份认证
客户端和服务端之间调用,可以通过加入证书的方式,实现调用的安全性。
SSL/TLS认证方式
TLS (Transport Layer Security,安全传输层),TLS 是建立在传输层 TCP 协议之上的协议,服务于应用层,它的前身是 SSL (Secure Socket Layer 安全套接字层),它实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能。
TLS 协议主要解决如下三个网络安全问题。
- 保密 (message privacy),保密通过加密 encryption 实现,所有信息都加密传输,第三方无法嗅探;
- 完整性 (message integrity),通过 MAC 校验机制,一旦被篡改,通信双方会立刻发现;
- 认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充;
生产环境可以购买证书(eg. 阿里云、腾讯)或者使用一些平台发放的免费证书(eg. 宝塔)
- key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。
- csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。
- crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。
- pem:是基于 Base64 编码的证书格式,扩展名包括 PEM、CRT 和 CER 。
首先通过 openssl 生成证书和私钥。
- 下载便捷安装包 https://slproweb.com/products/Win32OpenSSL.html 一直下一步即可
- 配置环境变量(路径到bin)
- 命令行测试
openssl
生成证书
- 新建目录
key
,在该目录下执行以下命令:
- 更改
openssl.cnf
将
openssl.cfg
复制到key
目录下
找到 [CA_default],打开
copy_extensions = copy
(就是把前面的#去掉)
找到 [ reg ],打开
reg_extensions = v3_req ...
找到 [v3_reg],添加
subjectAltName =@alt_names
添加新的标签 [ alt_names ],和标签字段
DNS.1 = *.kuangstudy.com
- 在
key
目录下执行以下命令:
- 生成证书私钥 test.key
openssl genpkey -algorithm RSA -out test.key
- 通过私钥 test.key 生成证书请求文件 test.csr (注意cfg和cnf)
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req
test.csr 是上面生成的证书请求文件。ca.crt/server,key 是 CA证书文件和 key,用来对 test.csr 进行签名认证。这两个文件在第一部分生成。
- 生成 SAN证书 pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
服务端
hello-server\main.go
:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "grpc-study/hello-server/proto"
"net"
)
// hello server
type server struct {
pb.UnimplementedSayHelloServer
}
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello " + req.RequestName}, nil
}
func main() {
// TSL认证
// 两个参数分别足 cretFile ,keyFile
// 自答名证书文件和私钥文件
creds, _ := credentials.NewServerTLSFromFile("C:\\workspace4Goland\\grpc-study\\key\\test.pem", "C:\\workspace4Goland\\grpc-study\\key\\test.key")
// 开启端口
listen, _ := net.Listen("tcp", ":9090")
// 创建grpc服务
grpcServer := grpc.NewServer(grpc.Creds(creds))
// 在grpc服务端中去注册我们自己编写的服务
pb.RegisterSayHelloServer(grpcServer, &server{})
// 启动服务
err := grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
}
}
客户端
hello-clinent\main.go
:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "grpc-study/hello-server/proto"
"log"
)
func main() {
creds, _ := credentials.NewClientTLSFromFile("C:\\workspace4Goland\\grpc-study\\key\\test.pem", "*.kuangstudy.com")
// 连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 建立连接
clinent := pb.NewSayHelloClient(conn)
// 执行rpc调用(这个方法在服务器端来实现并返回结果)
resp, _ := clinent.SayHello(context.Background(), &pb.HelloRequest{RequestName: "Shigy"})
fmt.Println(resp.GetResponseMsg())
}
Token认证
先看一个 gRPC 提供的一个接口,这个接口中有两个方法,接口位于 credentials 包下,这个接口需要客户端来实现
type PerRPCCredentials interface {
GetRequestMetadata(ctx context.Context, uri ...string)(map[string]string, error)
RequireTransportSecurity bool
}
- 第一个方法作用是获取元数据信息,也就是客户端提供的key,value对,context 用于控制超时和取消,uri 是请求入口处的 uri
- 第二个方法的作用是否需要基于 TLS 认证进行安全传输,如果返回值是 true,则必须加上 TLS 验证,返回值是 false 则不用
gRPC 将各种认证方式浓缩统一到一个凭证(credentials)上,可以单独使用一种凭证,比如只使用 TLS 凭证或者只使用自定义凭证,也可以多种凭证组合,gRPC 提供统一的 API 验证机制,使研发人员使用方便,这也是 gRPC 设计的巧妙之处。