gRPC
概念
什么是 gRPC
在了解gRPC前,我们先了解一下什么是 RPC,RPC
是 Remote Procedure Call
的缩写,即远程调用过程
。简单来说就是,有两台服务器 A、B。一个应用部署在 A 服务器上,另一个应用部署在 B 服务器上,现在A
服务器上的应用
需要调用
B
服务器上应用提供的方法、函数
,但是由于它们不在同一个内存空间
,不能直接调用
,所以需要通过网络互相传递信息
。
gRPC其实就是RPC框架的一种,gRPC
一开始是由Google开发,是一款语言中立
、平台中立
、开源
的远程调用(RPC)系统。
gRPC
是一个高性能
、开源
和通用
的RPC框架,面向移动和 HTTP/2 设计
。gRPC基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单TCP连接上的多复用请求等待。这些特性使得其在移动设备上表现更好,更省电和节省空间占用,
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同机器上服务端应用的方法,使得我们能更容易的创建分布式应用和服务。gRPC 与众多RPC系统一样,gRPC基于定义服务的思想,定义一个服务,指定可以远程调用的方法及其参数和返回类型。在服务端实现此接口,并运行 gRPC 服务器来处理客户端调用。在客户端有一个存根(在某些语言中称为客户端),它提供与服务器相同的方法。
协议缓冲区(Protocol Buffers)
gRPC默认情况下使用协议缓冲区(Protocol Buffers)
,这是Google开源的一套成熟的结构数据序列化机制(当然它也可以使用其它数据格式,如Json)。下面介绍一下它是如何工作的。
- 使用协议缓冲区的第一步:定义要在原型文件中序列化的数据结构,这是一个带扩展名的普通文本文件。协议缓冲区数据被结构化为消息,其中每个消息都是包含一系列称为字段的
name-value
键值对的信息的小逻辑记录。下面是一个简单的例子:.proto
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
- 然后,一旦指定了数据结构,就可以使用协议缓冲区编译器从原型定义中以首选语言生成数据访问类,它们为每个字段提供简单的访问器。例如
name()
和set_name
,以及将整个结构序列化/解析为原始字节的方法。因此,例如,如果你选择的语言是c++,那么在上面的示例中运行编译器将生成一个名为 Person的类。然后你可以在应用程序中使用该类来填充、序列化和检索协议缓冲区消息。
在普通的原型文件中定义gRPC服务,使用RPC方法参数和返回类型指定为协议缓冲区消息:
// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
hello gRPC
1、下载安装 Protocol Buffers
- 下载 Protocol Buffers
官网地址
根据个人电脑选择安装,我的电脑是windows64位
- 解压下载好的安装包,解压后的安装目录如下图所示:
- 配置环境变量
保存退出。 - 检查是否有效 打开命令提示符,输入
protoc
命令,出现以下信息即可
新建Go项目,在 Terminal 中执行后续步骤
2、安装 gRPC 核心库
go get google.golang.org/grpc
3、安装 protocol 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
执行完上述命令后,可以在 $GOWORKS/bin的路径查看,会新增以下两个应用程序
proto文件简介
此处介绍proto3,详细介绍请至官网Protocol Buffers Documentation
message
protobuf 中定义一个消息类型是通过关键字 message
字段指定,消息即:需要传输的数据格式的定义,类似Go语言中的struct
。
下面假设我们需要定义 搜索请求消息格式,其中每个搜索请求都有一个查询字符串、感兴趣的特点结果页面以及每个结果的数量 页。下面是用于定义消息类型的文件。.proto
syntax = "proto3"; // 声明使用的语法
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
- 上述文件第一行,指定你正在使用的语法:如果不这样做,那么protocol buffer compiler(协议缓冲区编译器)将假定你使用的是
Proto2
。这必须是文件的第一个非空、非注释行。.proto3
- 消息定义指定三个字段。
数据类型 名称 = 字段编号
消息号
在消息体的定义中,每个字段都必须有一个唯一的标识号
,标识号是【1,2^29-1】范围内的一个整数
。
字段规则
required
:消息体中必填字段,不设置会导致编码异常。在 proto2 中使用,在proto3 中被删除
optional
:消息体中可选字段。proto3 中没有 required、optional等说明关键字,都默认为 optional
repeated
:消息体中可重复字段,重复的值的顺序会被保留在go中,重复的会被定义为切片
map
:成对的键/值字段类型
嵌套消息
message PersonInfo {
message Person {
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
}
如果要在它的父消息类型的外部重用这个消息类型,需要PersonInfo.Person的形式使用它,例如:
message PersonMessage {
PersonInfo.Person info = 1;
}
服务定义
如果想将消息类型用在RPC系统中,可以在.proto 文件中定义一个RPC服务接口
,protocol buffer 编译器将会根据所选择的不同语言生成服务接口代码及存根
service SearchService {
# rpc 服务函数名 [参数] 返回 [返回参数]
rpc Search(SearchRequest) returns (SearchResponse)
}
上述表示定义了一个RPC服务,该方法接收SearchRequest返回SearchResponse
编写示例
创建并编写.proto
文件
- 在项目中创建以下所示文件目录
hello-client:代表了客户端
hello-server:代表了服务端
客户端和服务端的hello.proto
文件内容一致,如下所示:
// 说明使用的是proto3语法
syntax = "proto3";
// 关于最后生成的go文件在哪个目录包哪个包中,.代表当前目录,service代表生成的go文件的包名是service。
option go_package = ".;service";
// 定义一个服务,此服务中需要有一个方法,这个方法可以接收客户端的参数,再返回服务的响应。
// 定义了一个service服务,名称为 HelloGrpc,这个服务中有一个rpc方法,名字是 HelloGrpc
service HelloGrpc {
rpc HelloGrpc(HelloRequest) returns (HelloResponse) {}
}
// message关键字,类似Go语言中的结构体
// 此处特别之处在于,变量后面的”值“。在这里并不是赋值,而是定义这个变量在这个message中的位置
message HelloRequest {
string requestName = 1;
// int64 age = 2;
}
message HelloResponse {
string responseMsg = 1;
}
编写完上述内容后,分别在hello-server/proto
和hello-client/proto
目录下执行以下命令:
# .代表当前目录 hello.proto代表目标文件
protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto
执行完后会发现新增了以下文件
服务端编写
步骤:
- 创建gRPC Server对象
- 将server(其包含需要被调用的服务端接口)注册到gRPC Server的内部注册中心。
- 创建Listen,监听TCP端口
- gRPC Server开始 listen.Accept 直到 Stop
完整示例如下:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"net"
pb "testGrpc/hello-server/proto"
)
type server struct {
pb.UnimplementedHelloGrpcServer
}
func (s *server) HelloGrpc(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello grpc"}, nil
}
func main() {
// 创建端口
listen, _ := net.Listen("tcp", ":9090")
// 创建gRPC服务
newServer := grpc.NewServer()
// 在gRPC服务的注册我们自己编写的服务
pb.RegisterHelloGrpcServer(newServer, &server{})
// 启动服务
err := newServer.Serve(listen)
if err != nil {
fmt.Printf("failed to server: %v", err)
return
}
}
客户端编写
步骤
- 创建与服务端的连接
- 创建server的客户端对象
- 发生 RPC 请求,等待同步响应,得到回调后返回响应结果
- 输出响应结果
完整示例如下:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
pb "testGrpc/hello-server/proto"
)
func main() {
// 连接到 server 端,此处禁用安全传输,没有加密和验证
clientConn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("连接server失败:%v", err)
}
defer clientConn.Close()
// 建立连接
client := pb.NewHelloGrpcClient(clientConn)
// 执行rpc调用(这个方法在服务器端来实现并返回结果)
response, _ := client.HelloGrpc(context.Background(), &pb.HelloRequest{RequestName: "hello client"})
fmt.Println(response.GetResponseMsg())
}
SSL/TLS认证方式
通过openssl生成证书和密钥
- 官网下载:https://www.openssl.org/source/
- 注意官网下载需要自行编译安装
- 也可以直接下载大佬做的便捷安装包:
- 配置环境变量:将其安装目录下的bin目录配置到环境变量
- 命令行测试
openssl
出现以下内容即配置成功
生成证书
- key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。
- csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。
- crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。
- pem:是基于Base64编码的证书格式,扩展名包括 PEM、CRT和CER。
1.生成私钥
# 1、生成私钥
openssl genrsa -out server.key 2048
2.生成证书
# 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]:jiangsu
# 城市名称
Locality Name (eg, city) []:nanjing
# 公司组织名称
Organization Name (eg, company) [Internet Widgits Pty Ltd]:GO
# 部门名称
Organization Unit Name (eg, section) []:go
# 服务器或者网站名称
Common Name (e.g. server FQDN or YOUR name) []:lhail
# 邮件
Email Address []:123456@163.com
3.生成 csr
# 3、生成 csr
openssl req -new -key server.key -out server.csr
4、修改配置文件
- 复制一份安装的 openssl 的 bin 目录里面的 openssl.cnf 文件到项目所在目录
- 更改 openssl.cnf (Linux是 openssl.cfg),需要修改的内容如下:
#1) 找到 [ CA_default ],打开 copy_extensions = copy (即去掉前面的#)
#2)找到 [ req ],打开 req_extensions = v3_req # The extensions to add to a certificate request
#3)找到 [ v3_req ],添加 subjectAltName = @alt_names
#4)添加新的标签 [ alt_names ],和标签字段
DNS.1 = *.lhail.com
5、生成证书私钥
# 生成证书私钥 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.cnf -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.cnf -extensions v3_req
TSL认证
前面的编写示例
中,我们没有使用安全传输,这样会存在一些安全问题,下面使用TSL认证。只需更改一下前面 服务端和客户端的代码即可,如下所示:
在此前先确保上面证书生成已经完成,完成后,我们会得到以下文件:
- 服务端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"net"
pb "testGrpc/hello-server/proto"
)
type server struct {
pb.UnimplementedHelloGrpcServer
}
func (s *server) HelloGrpc(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello grpc"}, nil
}
func main() {
// TSL 认证
// 两个参数分别是 certFile, keyFile
creds, _ := credentials.NewServerTLSFromFile("E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.pem",
"E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.key")
// 创建端口
listen, _ := net.Listen("tcp", ":9090")
// 创建gRPC服务
//newServer := grpc.NewServer()
newServer := grpc.NewServer(grpc.Creds(creds))
// 在gRPC服务的注册我们自己编写的服务
pb.RegisterHelloGrpcServer(newServer, &server{})
// 启动服务
err := newServer.Serve(listen)
if err != nil {
fmt.Printf("failed to server: %v", err)
return
}
}
- 客户端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
pb "testGrpc/hello-server/proto"
)
func main() {
// 注意此处的 "*.lhail.com" 应该是在浏览器动态获取,此处仅为示例,所以写一个固定的,若获取的地址非 openssl.cnf 配置文件中配置的地址,将获取不到数据
creds, _ := credentials.NewClientTLSFromFile("E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.pem", "*.lhail.com")
// 连接到 server 端,此处禁用安全传输,没有加密和验证
//clientConn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
clientConn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("连接server失败:%v", err)
}
defer clientConn.Close()
// 建立连接
client := pb.NewHelloGrpcClient(clientConn)
// 执行rpc调用(这个方法在服务器端来实现并返回结果)
response, _ := client.HelloGrpc(context.Background(), &pb.HelloRequest{RequestName: "hello client"})
fmt.Println(response.GetResponseMsg())
}
Token认证
gRPC提供了一个接口,这个接口位于 credentials 包下,这个接口需要客户端来实现
type PerRPCCredentials interface {
// 此方法作用是获取元数据信息,即客户端提供的 key,value 对,context 用于控制超时和取消,uri是请求入口出的uri
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// 此方法作用是是否需要基于TLS认证进行安全传输,如果返回true,则必须加上TLS验证,返回值是false则不用
RequireTransportSecurity() bool
}
- 客户端实现
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
pb "testGrpc/hello-server/proto"
)
type ClientTokenAuth struct {
}
func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appId": "lhail",
"appKey": "123456",
}, nil
}
func (c ClientTokenAuth) RequireTransportSecurity() bool {
return false
}
func main() {
// 注意此处的 "*.lhail.com" 应该是在浏览器动态获取,此处仅为示例,所以写一个固定的,若获取的地址非 openssl.cnf 配置文件中配置的地址,将获取不到数据
//creds, _ := credentials.NewClientTLSFromFile("E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.pem", "*.lhail.com")
// 连接到 server 端,此处禁用安全传输,没有加密和验证
//clientConn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
//clientConn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(creds))
var opts []grpc.DialOption
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
opts = append(opts, grpc.WithPerRPCCredentials(new(ClientTokenAuth)))
clientConn, err := grpc.Dial("127.0.0.1:9090", opts...)
if err != nil {
log.Fatalf("连接server失败:%v", err)
}
defer clientConn.Close()
// 建立连接
client := pb.NewHelloGrpcClient(clientConn)
// 执行rpc调用(这个方法在服务器端来实现并返回结果)
response, err2 := client.HelloGrpc(context.Background(), &pb.HelloRequest{RequestName: "hello client"})
if err2 != nil {
fmt.Println(err2)
return
}
fmt.Println(response.GetResponseMsg())
}
- 服务端实现
package main
import (
"context"
"errors"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"net"
pb "testGrpc/hello-server/proto"
)
type server struct {
pb.UnimplementedHelloGrpcServer
}
//func (s *server) HelloGrpc(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// return &pb.HelloResponse{ResponseMsg: "hello grpc"}, nil
//}
// HelloGrpc 业务
func (s *server) HelloGrpc(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// 获取元数据信息
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("未发现 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]
}
if appId != "lhail" || appKey != "123456" {
return nil, errors.New("token 错误")
}
return &pb.HelloResponse{ResponseMsg: "hello " + in.RequestName}, nil
}
func main() {
// TSL 认证
// 两个参数分别是 certFile, keyFile
//creds, _ := credentials.NewServerTLSFromFile("E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.pem", "E:\\Code\\GoProjects\\src\\testGrpc\\ssl-key\\test.key")
// 创建端口
listen, _ := net.Listen("tcp", ":9090")
// 创建gRPC服务
newServer := grpc.NewServer()
//newServer := grpc.NewServer(grpc.Creds(creds))
// 在gRPC服务的注册我们自己编写的服务
pb.RegisterHelloGrpcServer(newServer, &server{})
// 启动服务
err := newServer.Serve(listen)
if err != nil {
fmt.Printf("failed to server: %v", err)
return
}
}