一、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>
*/