一、http与http2及https
在介绍 grpc 之前先了解下 http、http2、https 的区别,因为 grpc 是基于 http2 协议标准设计开发的
1. 网络协议结构
ISO/OSI七层网络协议结构: 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层 (自上而下)
TCP/IP传输协议层级结构: 应用层、运(传)输层、网际(络)层、网络接口层(又称链路层)
2. http
http 协议是位于 TCP/IP 体系结构中的应用层协议,它是万维网的数据通信的基础,也是最常用的协议,默认端口是80,默认版本就是 http1.1
http的连接建立过程:
浏览器请求,先通过dns解析域名的ip地址
通过tcp三次握手建立tcp连接
发起http请求
目标服务器接受到http请求并处理
目标服务器往浏览器发送http响应
浏览器解析并渲染页面
http 是面向报文传输,报文由请求行、首部、实体主体组成,它们之间由CRLF分隔开
3. https
https 是最流行的 http 安全形式,由网景公司首创,所有主流的浏览器和服务器都支持此协议
使用 https 时,所有的 http 请求和响应数据在发送之前,都要进行加密(对称加密和非对称加密)。加密可以使用 SSL/TLS
4. http2
http2 是 http1.x 的扩展,而非替代,所以 http 的语义不变,提供的功能不变,http 方法、状态码、URL 和首部字段等这些核心概念也不变
之所以要递增一个大版本到 2.0,主要是因为它改变了客户端与服务器之间交换数据 的方式。HTTP2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP1.x 服务器及客户端
现在的主流浏览器 HTTP2.0 的实现都是基于 SSL/TLS 的,也就是说使用 HTTP/2 的网站都是 HTTPS 协议的
HTTP/1.1 是以文本分隔的,解析 HTTP/1.1 需要不断地读入字节,直到遇到分隔符 CRLF 为止,如果客户端想发送多个并行的请求,那么必须使用多个 TCP 连接
HTTP/2 是基于帧的协议,所有的请求和响应都在同一个 TCP 连接上发送:客户端和服务器把 HTTP 消息分解成多个帧,然后乱序发送,最后在另一端再根据流 ID 重新组合起来。
二、grpc详解
1. 介绍
grpc 是一个高性能、开源、通用的 rpc 框架,由Google推出,基于HTTP2协议标准设计开发,默认采用 Protocol Buffers(protobuf) 数据序列化协议,支持多种开发语言,可在任意环境运行
官方文档:https://grpc.io/docs/languages
中文文档:https://doc.oschina.net/grpc
2. go安装grpc
-
安装两个组件
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
安装成功后,
$GOPATH/bin
目录下,会生成两个文件。如果没有配置系统环境变量,需要将此目录配置下环境变量,否则会影响后续命令执行 -
安装 Protocol Buffers
下载地址:https://github.com/protocolbuffers/protobuf/releases
根据自己的操作系统去下载安装,并且配置环境变量。配置好后,可以使用
protoc --version
进行测试 -
示例参考(可选)
官方文档上提供了示例参考,可下载查看
git clone -b v1.58.2 --depth 1 https://github.com/grpc/grpc-go cd grpc-go/examples/helloworld
3. grpc支持的四种类型服务方法
具体解释查看官方文档
简单 RPC
服务端流 RPC
客户端流 RPC
双向流 RPC
4. 一个简单rpc的应用示例
客户端向服务端发送单个请求,取回单个响应,就像普通的函数调用一样
-
在项目中集成 grpc 包
mkdir grpc cd grpc go mod init go get google.golang.org/grpc
-
创建一个 .proto 的文件
// 指定当前proto语法的版本, 有2和3 syntax = "proto3"; // option go_package = "path;name"; // path: 生成的go文件存放地址, .表示当前目录 // name: 生成go文件的包名 option go_package = ".;hello"; // 指定生成的go文件的package package hello; // 定义服务 service Hello { // 定义方法 rpc SayHello (HelloRequest) returns (HelloResponse) {} } // 定义请求 // 数字为序列号,表示这个变量在message中的位置 message HelloRequest { string name = 1; } // 定义响应 message HelloResponse { int32 code = 1; string message = 2; bytes data = 3; }
-
生成 .pb.go 文件
protoc --go_out=. --go-grpc_out=. hello.proto // 或者 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto
执行成功会在指定目录下生成两个文件
hello.pb.go
和hello_grpc.pb.go
-
编写服务端代码
package main import ( "context" pb "design/grpc/proto" "fmt" "google.golang.org/grpc" "net" ) type helloServer struct { pb.UnimplementedHelloServer } func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { resp := &pb.HelloResponse{ Code: 200, Message: "ok", Data: []byte("hello : " + req.Name), } return resp, nil } func main() { // 监听地址和端口 listen, err := net.Listen("tcp", ":8001") if err != nil { fmt.Println(err) } // 创建grpc服务 ser := grpc.NewServer() // 注册服务 pb.RegisterHelloServer(ser, new(helloServer)) // 启动服务 err = ser.Serve(listen) if err != nil { fmt.Println(err) } }
-
编写客户端代码
package main import ( "context" pb "design/grpc/proto" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { conn, err := grpc.Dial("127.0.0.1:8001", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { fmt.Println(err) } defer conn.Close() client := pb.NewHelloClient(conn) resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"}) if err != nil { mt.Println(err) } fmt.Println(resp) // code:200 message:"ok" data:"hello : ricky" }
三、protobuf详解
1. 介绍
Protocol Buffers 简称 protobuf,是谷歌开发的一款无关平台、语言、可扩展、轻量级高效的序列化结构的数据格式,用于将自定义数据结构序列化成字节流和将字节流反序列化为数据结构。可以把它当成一个代码生成工具以及序列化工具,不同应用之间互相通信的数据交换格式,只要实现相同的协议格式,即后缀为proto文件被编译成不同的语言版本,加入各自的项目中,这样不同的语言可以解析其它语言通过 Protobuf 序列化的数据。
2. 优势
序列化后体积比JSON和XML小,适合网络传输
序列化反序列化速度快,比JSON的处理速度快
消息格式升级和兼容性较好
支持跨平台多语言
3. message语法
message定义一个消息类型/数据结构,和 Java的class,Go语言中的struct类似。命名采用驼峰命名方式
在一个proto文件中message可以定义多个,也支持嵌套及使用import引入其他proto文件的message
字段格式:字段规则 | 数据类型 | 字段名称 | = | 字段标识号 | [字段默认值]
-
字段规则:
required:消息体中必填字段,不设置会导致编码解码的异常。不设置默认这个
optional:可选字段,值可以存在,也可以为空
repeated:可重复字段,可以存在多个(包括0个)。重复的值的顺序会被保留,对应 java 的数组或者go语言的slice
-
数据类型:
常见的映射关系如下
proto C++ Java Python Go PHP double double double float float64 float float float float float float32 float int32 int32 int int int32 integer int64 int64 long ing/long[3] int64 integer/string[5] uint32 uint32 int[1] int/long[3] uint32 integer uint64 uint64 long[1] int/long[3] uint64 integer/string[5] sint32 int32 int intj int32 integer sint64 int64 long int/long[3] int64 integer/string[5] fixed32 uint32 int[1] int uint32 integer fixed64 uint64 long[1] int/long[3] uint64 integer/string[5] sfixed32 int32 int int int32 integer sfixed64 int64 long int/long[3] int64 integer/string[5] bool bool boolean bool bool bool string string String str/unicode string string bytes string ByteString str []byte string 标识号:在消息的定义中,每个字段等号后面都有唯一的标识号,用于在反序列化过程中识别各个字段的,一旦开始使用就不能改变。标识号从整数1开始,依次递增,每次增加1,标识号的范围为1~2^29 – 1,其中 [19000-19999] 为 Protobuf 协议预留字段,开发者不建议使用该范围的标识号;一旦使用,在编译时Protoc编译器会报出警告
-
默认值:protobuf3删除了protobuf2中用来设置默认值的default关键字,为各类型定义的默认值
类型 默认值 bool false 数值 0 string "" enum 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0; message 不是null,而是DEFAULT_INSTANCE -
更新规则:message定义以后如果需要进行修改,为了保证之前的序列化和反序列化能够兼容新的message,message的修改需要满足以下规则
不可以修改已存在域中的标识号
所有新增添的域必须是 optional 或者 repeated
非required域可以被删除,但是这些被删除域的标识号不可以再次被使用
非required域可以被转化,转化时可能发生扩展或者截断,此时标识号和名称都是不变的
sint32和sint64是相互兼容的
fixed32兼容sfixed32;fixed64兼容sfixed64
optional兼容repeated,发送端发送repeated域,用户使用optional域读取, 将会读取repeated域的最后一个元素
4. 序列化原理
在序列化时,Protobuf 按照TLV的格式序列化每一个字段,T即Tag,也叫Key;V是该字段对应的值value;L是Value的长度,如果一个字段是整形,这个L部分会省略
序列化后的Value是按原样保存到字符串或者文件中,Key按照一定的转换条件保存起来,序列化后的结果就是 KeyValueKeyValue…依次类推的样式
四、grpc认证
grpc认证方式与传统的rpc认证方式无差别,因为grpc本质仍然是rpc。grpc默认内置了两种认证方式:
SSL/TLS认证
Token认证
1. SSL/TLS认证实现
-
第一步,安装OpenSSL
官方下载地址: https://www.openssl.org/source
windows下载:http://slproweb.com/products/Win32OpenSSL.html
我这里下载的win64 v3.1.4版本,安装完成后记得配置下环境变量
-
第二步,配置证书
配置 ca.conf 证书文件
[ req ] default_bits = 4096 distinguished_name = req_distinguished_name [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = CN stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = beijing localityName = Locality Name (eg, city) localityName_default = beijing organizationName = Organization Name (eg, company) organizationName_default = ricky commonName = Common Name (e.g. server FQDN or YOUR name) commonName_default = ricky commonName_max = 64
配置 server.conf 证书文件
[ req ] default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = req_ext [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = CN stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = beijing localityName = Locality Name (eg, city) localityName_default = beijing organizationName = Organization Name (eg, company) organizationName_default = ricky commonName = Common Name (e.g. server FQDN or YOUR name) commonName_default = ricky commonName_max = 64 [ req_ext ] subjectAltName = @alt_names [alt_names] DNS.1 = grpc IP = 127.0.0.1
生成CA根证书
openssl genrsa -out ca.key 4096 openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
生成Server证书
openssl genrsa -out server.key 2048 openssl req -new -sha256 -out server.csr -key server.key -config server.conf openssl x509 -req -in ca.csr -out ca.crt -CA ca.crt -CAkey ca.key –CAcreateserial openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -in server.csr -out server.pem -extensions req_ext -extfile server.conf
全部生成完后,目录下会存在这些文件
-
第三步,go中实现秘钥的配置
修改服务端
package main import ( "context" pb "design/grpc/proto" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "net" ) type helloServer struct { pb.UnimplementedHelloServer } func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { resp := &pb.HelloResponse{ Code: 200, Message: "ok", Data: []byte("hello : " + req.Name), } return resp, nil } func main() { // 监听地址和端口 listen, err := net.Listen("tcp", ":8001") if err != nil { fmt.Println(err) } // 新增一步,与之前区别的地方 creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key") if err != nil { fmt.Println(err) } // 创建grpc服务 ser := grpc.NewServer(grpc.Creds(creds)) // 注册服务 pb.RegisterHelloServer(ser, new(helloServer)) // 启动服务 err = ser.Serve(listen) if err != nil { fmt.Println(err) } }
修改客户端
package main import ( "context" pb "design/grpc/proto" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) func main() { // 新增一步,与之前区别的地方 creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "grpc") if err != nil { fmt.Println(err) } conn, err := grpc.Dial("127.0.0.1:8001", grpc.WithTransportCredentials(creds)) if err != nil { fmt.Println(err) } defer conn.Close() client := pb.NewHelloClient(conn) resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"}) if err != nil { mt.Println(err) } fmt.Println(resp) // code:200 message:"ok" data:"hello : ricky" }
五、grpc拦截器
1. 介绍
grpc 服务端和客户端都提供了拦截器(interceptor)功能,功能类似中间件(middleware),很适合在这里处理验证、日志等流程
2. 案例
使用上述SSL/TLS认证的grpc代码,加入拦截器再实现一个自定义token验证
服务端:
package main
import (
"context"
pb "design/grpc/proto"
"errors"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"net"
)
type helloServer struct {
pb.UnimplementedHelloServer
}
func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := &pb.HelloResponse{
Code: 200,
Message: "ok",
Data: []byte("hello : " + req.Name),
}
return resp, nil
}
func main() {
// 监听地址和端口
listen, err := net.Listen("tcp", ":8001")
if err != nil {
fmt.Println(err)
}
// 新增一步,与之前区别的地方
creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
if err != nil {
fmt.Println(err)
}
var opt []grpc.ServerOption
opt = append(opt, grpc.Creds(creds)) // 写入SSL/TLS认证
opt = append(opt, grpc.UnaryInterceptor(interceptor)) // 写入拦截器
// 创建grpc服务
ser := grpc.NewServer(opt...)
// 注册服务
pb.RegisterHelloServer(ser, new(helloServer))
// 启动服务
err = ser.Serve(listen)
if err != nil {
fmt.Println(err)
}
}
// 拦截器方法
func interceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
err = auth(ctx)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
// 自定义验证
func auth(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
fmt.Println(md)
if !ok {
return errors.New("信息参数有误")
}
var token string
if val, ok := md["token"]; ok {
fmt.Println(val)
token = val[0]
}
if token != "123456" {
return errors.New("token 认证失败")
}
return nil
}
客户端:
package main
import (
"context"
pb "design/grpc/proto"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type credentialsToken struct{}
// GetRequestMetadata 生成自定义验证的信息,如token
func (c *credentialsToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"token": "123456", // 具体token生成就不写了,直接写死一个
}, nil
}
// RequireTransportSecurity 是否开启SSL/TLS认证
// true:则两个认证方式同时使用;
// false:不开启SSL/TLS认证,只使用自定义认证
func (c *credentialsToken) RequireTransportSecurity() bool {
return true
}
func main() {
// 新增一步,与之前区别的地方
creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "grpc")
if err != nil {
fmt.Println(err)
}
var opt []grpc.DialOption
opt = append(opt, grpc.WithPerRPCCredentials(new(credentialsToken))) // 写入自定义认证
opt = append(opt, grpc.WithTransportCredentials(creds)) // 写入SSL/TLS认证
opt = append(opt, grpc.WithUnaryInterceptor(cInterceptor)) // 写入拦截器
conn, err := grpc.Dial("127.0.0.1:8001", opt...)
if err != nil {
fmt.Println(err)
}
defer conn.Close()
client := pb.NewHelloClient(conn)
resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"})
if err != nil {
fmt.Println(err)
}
fmt.Println(resp)
}
// 拦截器方法
func cInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
fmt.Println("client interceptor")
return invoker(ctx, method, req, reply, cc, opts...)
}
/**
token失败,会输出:
client interceptor
rpc error: code = Unknown desc = token 认证失败
<nil>
*/