gRPC(狂神说)
视频地址:【狂神说】gRPC最新超详细版教程通俗易懂 | Go语言全栈教程_哔哩哔哩_bilibili
1、gRPC介绍
单体架构
- 一旦某个服务宕机,会引起整个应用不可用,隔离性差
- 只能整体应用进行伸缩,浪费资源,可伸缩性差
- 代码耦合在一起,可维护性差
微服务架构:
解决了单体架构的弊端,但同时引入了新的问题
- 代码冗余
- 服务和服务之间存在调用关系
服务拆分后,服务和服务之间发生的是进程和进程之间的调用,服务器和服务器之间的调用,那么就需要发起网络调用。
网络调用我们能立马想起的就是http,但是在微服务架构中,http虽然便捷方便,但性能较低,这时候就需要引入RPC(远程过程调用),通过自定义协议发起TCP调用,来加快传输效率。
文档:Documentation | gRPC
中文文档:gRPC 官方文档中文版_V1.0 (oschina.net)
RPC的全称是Remote Procedure Call,远程过程调用。这是一种协议,是用来屏蔽分布式计算中的各种调用细节,使得你可以像是本地调用一样直接调用一个远程的函数。
客户端
与 服务端
沟通的过程
- 客户端 发送数据(以字节流的方式)
- 服务端 接受并解析。 根据约定知道要执行什么。然后把结果返回给客户
RPC:
- RPC就是将上述过程封装,使其操作更加优化
- 使用一些大家都认可的协议 使其规范化
- 做成一些框架。直接或间接产生利益
而gRPC又是什么呢?用官方的话来说
A high performance, open source universal RPC framework
在gRPC中,我们称调用方为client,被调用方为server。跟其他的RPC框架一样,gRPC也是基于“服务定义“的思想。简单的来讲,就是我们通过某种方式来描述一个服务,这种描述方式是语言无关的。在这个"服务定义"的过程中,我们描述了我们提供的服务服务名是什么,有哪些方法可以被调用,这些方法有什么样的入参,有什么样的回参。
也就是说,在定义好了这些服务、这些方法之后,**gRPC会屏蔽底层的细节,client只需要直接调用定义好的方法,就能拿到预期的返回结果。对于server端来说,还需要实现我们定义的方法。**同样的,gRPC也会帮我们屏蔽底层的细节,我们只需要实现所定义的方法的具体逻辑即可。
你可以发现,在上面的描述过程中,所谓的”“服务定义",就跟定义接口的语义是很接近的。我更愿意理解为这是一种"约定”,双方约定好接口,然后server实现这个接口,client调用这个接口的代理对象。至于其他的细节,交给gRPC。
此外,gRPC还是语言无关的
。你可以用C++作为服务端,使用Golang、java等作为客户端。为了实现这一点,我们在"定义服务“和在编码和解码的过程中,应该是做到语言无关的。
因此,gRPC使用了Protocol Buffss。这是谷歌开源的一套成熟的数据结构序列化机制。
你可以把他当成一个代码生成工具以及序列化工具。这个工具可以把我们定义的方法,转换成特定语言的代码。比如你定义了一种类型的参数。他会帮你转换成Golang中的struct 结构体,你定义的方法,他会帮你转换成func 图数。此外,在发送请求和接受响应的时候,这个工具还会完成对应的编码和解码工作,将你即将发送的数据编码成gRPC能够传输的形式,又或者将即将接收到的数据解码为编程语言能够理解的数据格式。
**序列化:**将数据结构或对象转换成二进制串的过程
**反序列化:**将在序列化过程中所产生的二进制串转换成数据结构或者对象的过程
protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场累。因为protobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
优势
- 序列化后体积相比Json和XML很小,适合网络传输
- 支持跨平台多语言
- 消息格式升级和兼容性还不错
- 序列化反序列化速度很快
2、安装Protobuf
安装地址:Releases · protocolbuffers/protobuf (github.com)
解压、为bin目录配置环境变量
验证:
cmd 输入:protoc
C:\Users\27184>protoc
Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH Specify the directory in which to search for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
If not found in any of the these directories,
the --descriptor_set_in descriptors will be
checked for required proto file.
--version Show version info and exit.
-h, --help Show this text and exit.
----------------------------------------------------------------------
安装gRPC核心库
go get google.golang.org/grpc
如果下载不下来,可能是没配置代理:
go env -w GOPROXY="https://goproxy.cn,direct"
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
3、Proto文件编写
//这是在说明我们使用的是proto3语法。
syntax ="proto3";
//这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中,.代表在当前目录生成,service代表了生成的go文件的包名是service。
option go_package =".;service";
//然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应。
//其实很容易可以看出,我仰定义了一个service,称为SayHe11o,这个服务中有一个rpc方法,名为SayHe1lo。
//这个方法会发送-个HelloRequest,然后返回一个HelloResponse。
service SayHello{
rpc SayHello(HelloRequest)returns (HelloResponse){}
}
//message关键字,其实你可以理解为Go1ang中的结构体。
//这里比较特别的是变量后面的“赋值”。注意,这里并不是赋值,而是在定义这个变量在这个message中的位置。
message HelloRequest{
string requestName = 1;
// int64 age = 2;
}
message HelloResponse{
string responseMsg = 1;
}
在编写完上面的内容后,在hello-server/proto目录下执行如下命令:
protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto
(如果出现了 --go_out: hello.pb.go: unparsable Go source: 1:3: illegal UTF-8 encoding (and 18 more errors) 的错误,请删除上述文件中的中文!!)
4、Proto文件介绍
message
message: protobuf中定义一个消息类型是通过关键字message字段指定的。消息就是需要传输的数据格式的定义
message 关键字类似于C++中的class,JAVA中的class,go中的struct
在消息中承载的数据分别对应于每一个字段,其中每个字段都有一个名字和一种类型
一个proto文件中可以定义多个消息类型
字段规则
required: 消息体中必填字段,不设置会导致编码异常。在protobuf2中使用,在protobuf3中被删去
optional: 消息体中可选字段。protobuf3没有了required,optional等说明关键字,都默认为optional
repeated: 消息体中可重复字段,重复的值的顺序会被保留在go中重复的会被定义为切片。
message HelloRequest{
string requestName = 1;
int64 age = 2;
repeated string name = 3;
}
消息号
在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[1,2^29-1]范围内的一个整数
嵌套消息
可以在其他消息类型中定义、使用消息类型,在下面的例子中,person消息就定义在Personlnfo消息内如
message PersonInfo{
message Person{
string name = 1;
int32 height = 2;
}
repeated Person info = 1;
}
服务定义
如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。
service searchService{
rpc Search(searchRequest)returns (searchResponse){}
}
上述代表表示,定义了一个RPC服务,该方法接收 searchRequest 返回 searchResponse
5、服务端代码编写
package main
import (
"context"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"net"
)
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() {
// 1. 开启端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
fmt.Printf("listen error: %v", err)
fmt.Println()
}
// 2. 创建grpc服务,在grpc服务端中注册
grpcServer := grpc.NewServer()
pb.RegisterSayHelloServer(grpcServer, &server{})
// 3.启动服务
err = grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
6、客户端代码编写
package main
import (
"context"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
)
func main() {
// 1. 连接到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()
// 2. 建立连接
client := pb.NewSayHelloClient(conn)
// 3. 执行rpc调用
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zwj"})
fmt.Printf(resp.ResponseMsg)
}
依次启动服务端、客户端即可。
7、认证及安全传输
gRPC 是一个典型的C/S模型,需要开发客户端和服务端,客户端与服务端需要达成协议,使用某一个确认的传输协议来传输数据,
gRPC默认是使用protobuf来作为传输协议,当然也是可以使用其他自定义的协议。
那么,客户端与服务端要通信之前,客户端如何知道自己的数据是发给哪一个明确的服务端呢?反过来,服务端是不是也需要有一种方式来弄清楚自己的数据要返回给谁呢?
那么就不得不提gRPC的认证,此处说到的认证,不是用户的身份认证,而是指多个server 和多个client之间,如何识别对方是谁,并且可以安全的进行数据传输
- SSL/TLS认证方式(采用http2协议)
- 基于Token的认证方式(基于安全连接)
- 不采用任何措施的连接,这是不安全的连接(默认采用http1)
- 自定义的身份认证
客户端和服务端之间调用,我们可以通过加入证书的方式,实现调用的安全性
TLS (Transport Laver Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure SocketLaver 安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。
TLS协议主要解决如下三个网络安全问题:
- 保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探
- 完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现;
- 认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充:
生产环境可以购买证书或者使用一些平台发放的免费证书
key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。
csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。
pem:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER。
一些名词解释:一次把http、https、tcp、udp、tls、ssl聊明白 - 二柒的博客 - 博客园 (cnblogs.com)
8、TLS认证实现
首先通过openssl生成证书和私钥
- 官网下载:https://www.openssl.org/source/
- 其他人做的便捷版安装包 Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions (slproweb.com)
- 我们使用 便捷版安装包,一直下一步即可
- 配置环境变量 D:\Environment\OpenSSL-Win64\bin
- 命令行测试 openssl
生成证书
#1、生成私钥
openssl genrsa -out server.key 2048
#2、生成证书全部回车即可,可以不填
openssl req -new -x509 -key server.key -out server.crt -days 36500
#国家名称
Country Name (2 letter code)[AU]:CN
#省名称
State or Province Name (full name)[Some-State]:GuangDong
#城市名称
Locality Name
(eg,city)[]Meizhou
#公司组织名称
organization Name (eg,company)[Internet widgits Pty Ltd]:Xuexiangban
#部门名称
organizational Unit Name (eg,section)[]go
#服务器or网站名称
Common Name (e.g.server FQDN or YOUR name)[]kuangstudy
#邮件
Email Address []:24736743@qq.com
#3、生成csr
openssl req -new -key server.key -out server.csr
更改openssl文件
#1)复制一份你安装的openss1的bin目录里面的openss1.cnf 文件到你项目所在的目录
#2)找到[ CA_default],打开 copy_extensions = copy (就是把前面的#去掉)
#3)找到[ req ],打开 req_extensions = v3_reg # The extensions to add to a certificate request
#4)找到[v3_req],添加 subjectAltName = @alt_names
#5)添加新的标签[ alt_names ],和标签字段
DNS.1 =*.kuangstudy.com
生成私钥信息
#生成证书私钥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进行签名认证。这两个文件在第一部分生成。
#上述一般会有警告信息:Ignoring -days without -x509; not generating a certificate,这个警告表明,在没有指定 -x509 选项的情况下,-days 选项将被忽略。-days 选项通常用于指定自签名证书(self-signed certificate)的有效期限,而不是 CSR。因此,如果你想生成一个自签名证书,你需要添加 -x509 选项,但是这样做下面的生成SAN证书会出现错误!!
openssl req -new -nodes -key test.key -out test.csr -x509 -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req
#生成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
服务端代码
package main
import (
"context"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"net"
"os"
)
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() {
cwd, err := os.Getwd()
if err != nil {
fmt.Println("Error getting current working directory:", err)
return
}
fmt.Println("Current working directory:", cwd)
cred, err := credentials.NewServerTLSFromFile("./key/test.pem", "./key/test.key")
if err != nil {
fmt.Println(err)
fmt.Println("NewServerTLSFromFile error")
}
// 1. 开启端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
fmt.Printf("listen error: %v", err)
fmt.Println()
}
// 2. 创建grpc服务,在grpc服务端中注册
grpcServer := grpc.NewServer(grpc.Creds(cred))
pb.RegisterSayHelloServer(grpcServer, &server{})
// 3.启动服务
err = grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
客户端代码
package main
import (
"context"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
)
func main() {
cred, err := credentials.NewClientTLSFromFile("./key/test.pem",
"*.kuangstudy.com")
if err != nil {
fmt.Println(err)
fmt.Println("NewClientTLSFromFile error")
}
// 1. 连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(cred))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 2. 建立连接
client := pb.NewSayHelloClient(conn)
// 3. 执行rpc调用
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zwj"})
fmt.Printf(resp.ResponseMsg)
}
9、自定义Token
我们先看一个gRPC提供我们的一个接口,这个接口中有两个方法,接口位于credentials 包下,这个接口需要客户端来实现
// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
// GetRequestMetadata gets the current request metadata, refreshing tokens
// if required. This should be called by the transport layer on each
// request, and the data should be populated in headers or other
// context. If a status code is returned, it will be used as the status for
// the RPC (restricted to an allowable set of codes as defined by gRFC
// A54). uri is the URI of the entry point for the request. When supported
// by the underlying implementation, ctx can be used for timeout and
// cancellation. Additionally, RequestInfo data will be available via ctx
// to this call. TODO(zhaoq): Define the set of the qualified keys instead
// of leaving it as an arbitrary string.
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
RequireTransportSecurity() bool
}
第一个方法作用是获取元数据信息,也就是客户端提供的key,alue对,context用于控制超时和取消,uri是请求入口处的uri
第二个方法的作用是否需要基于 TLS 认证进行安全传输,如果返回值是true,则必须加上TLS验证,返回值是false则不用。
客户端代码
package main
import (
"context"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
)
type ClientTokenAuth struct {
}
func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appId": "kuangshen",
"appKey": "123123",
}, nil
}
func (c ClientTokenAuth) RequireTransportSecurity() bool {
return false
}
func main() {
var dials []grpc.DialOption
dials = append(dials, grpc.WithTransportCredentials(insecure.NewCredentials()))
dials = append(dials, grpc.WithPerRPCCredentials(new(ClientTokenAuth)))
//cred, err := credentials.NewClientTLSFromFile("./key/test.pem",
// "*.kuangstudy.com")
//if err != nil {
// fmt.Println(err)
// fmt.Println("NewClientTLSFromFile error")
//}
// 1. 连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("127.0.0.1:9090", dials...)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 2. 建立连接
client := pb.NewSayHelloClient(conn)
// 3. 执行rpc调用
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zwj"})
fmt.Printf(resp.ResponseMsg)
}
服务端代码
package main
import (
"context"
"errors"
"fmt"
pb "gRPC/hello-server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"net"
)
type server struct {
pb.UnimplementedSayHelloServer
}
// SayHello 业务代码
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("not found token")
}
var appId string
var appKey string
if v, ok := md["appid"]; ok {
appId = v[0]
}
if v, ok := md["appkey"]; ok {
appKey = v[0]
}
fmt.Printf("appId:%s,appKey:%s", appId, appKey)
fmt.Println()
if appId != "kuangshen" || appKey != "123123" {
return nil, errors.New("token is not valid")
}
return &pb.HelloResponse{ResponseMsg: "hello" + " " + req.RequestName + " " + appId + " " + appKey}, nil
}
func main() {
//cred, err := credentials.NewServerTLSFromFile("./key/test.pem", "./key/test.key")
//if err != nil {
// fmt.Println(err)
// fmt.Println("NewServerTLSFromFile error")
//}
// 1. 开启端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
fmt.Printf("listen error: %v", err)
fmt.Println()
}
// 2. 创建grpc服务,在grpc服务端中注册
grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
pb.RegisterSayHelloServer(grpcServer, &server{})
// 3.启动服务
err = grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
gRPC将各种认证方式浓缩统一到一个凭证(credentials)上,可以单独使用一种凭证,比如只使用TLS凭证或者只使用自定义凭证,也可以多种凭证组合,gRPC提供统一的API验证机制,使研发人员使用方便,这也是gRPC设计的巧妙之处。
10、小结
期待kubernetes呜呜呜