什麼是grpc
詳細文檔:
https://doc.oschina.net/grpc?t=58008
- gRpc 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。目前提供 C、Java 和 Go 語言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
- gRPC 基於 HTTP/2 標準設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多複用請求等特。這些特性使得其在移動設備上表現更好,更省電和節省空間佔用。
- gRPC 默認使用 protocol buffers
- 參考:grpc官方文檔英文版 和 grpc官方文檔中文版
- github : https://github.com/grpc/grpc-go
爲什麼我們要用grpc
- 生態好:背靠Google。還有比如nginx也對grpc提供了支持,參考鏈接
- 跨語言:跨語言,且自動生成sdk
- 性能高:比如protobuf性能高過json, 比如http2.0性能高過http1.1
- 強類型:編譯器就給你解決了很大一部分問題
- 流式處理(基於http2.0):支持客戶端流式,服務端流式,雙向流式
grpc 的優點是怎麼實現的
grpc性能高:protobuf爲什麼比json性能高?
什麼是protobuf?
- Protobuf是由Google開發的二進制格式,用於在不同服務之間序列化數據。是一種IDL(interface description language)語言
他比json快多少?
- 快六倍,參考鏈接
爲什麼protobuf比json快?
- protobuf的二進制數據流如下圖
json格式
{"content":"test","user":"test","user_id":"test"}
- 對比json數據和protobuf數據格式可以知道
- 體積小-無需分隔符:TLV存儲方式不需要分隔符(逗號,雙引號等)就能分隔字段,減少了分隔符的使用
- 體積小-空字段省略:若字段沒有被設置字段值,那麼該字段序列化時的數據是完全不存在的,即不需要進行編碼,而json會傳key和空值的value
- 體積小-tag二進制表示:是用字段的數字值然後轉換成二進制進行表示的,比json的key用字符串表示更加省空間
- 編解碼快:tag的裏面存儲了字段的類型,可以直接知道value的長度,或者當value是字符串的時候,則用length存儲了長度,可以直接從length後取n個字節就是value的值,而如果不知道value的長度,我們就必須要做字符串匹配
- 細化了解protobuf的編碼可以去看:varint 和 zigzag編碼方式
grpc性能高:http2.0爲什麼比http1.1性能高?
多路複用
- http2.0和http 1.* 還有 http1.1pipling的對比
- 示意圖
-
http/1.* :一次請求,一個響應,建立一個連接用完關閉,每一個請求都要建立一個連接
-
http1.1 pipeling: Pipeling解決方式爲,若干個請求排隊串行化單線程處理,後面的請求等待前面請求的返回才能獲得執行機會,一旦有某請求超時等,後續請求只能被阻塞,毫無辦法,也就是人們常說的線頭阻塞
-
http2: 多個請求可同時在一個連接上並行執行。某個請求任務耗時嚴重,不會影響到其它連接的正常執行
-
grpc 多路複用還有哪些優點
-
- 減少了tcp的連接,降低了服務端和客戶端對於內存,cpu等的壓力
- 減少了tcp的連接,保證了不頻繁觸發tcp重新建立,這樣就不會頻繁有慢啓動
- 減少了tcp的連接,使網絡擁塞情況得以改善
-
爲什麼http/1.1不能實現多路複用而http2.0可以?
-
- 因爲http/1.1傳輸是用的文本,而http2.0用的是二進制分幀傳輸
2. 頭部壓縮
-
固定字段壓縮:http可以通過http對body進行gzip壓縮,這樣可以節省帶寬,但是報文中header也有很多字段沒有進行壓縮,比如cookie, user agent accept,這些有必要進行壓縮
-
避免重複:大量請求和響應的報文裏面又很多字段值是重複的,所以有必要避免重複性
-
編碼改進:字段是ascii編碼,效率低,改成二進制編碼可以提高
-
以上通過HPACK算法來進行實現,算法主要包含三個部分
-
- 靜態字典:將常用的header字段整成字典,比如{":method":"GET"} 就可以用單個數字 2來表示
- 動態字典:沒有在靜態字典裏面的一些頭部字段,則用動態字典
- Huffman 編碼: 壓縮編碼
3. 二進制分幀
-
在二進制分幀層上,HTTP 2.0 會將所有傳輸的信息分割爲更小的消息和幀,並對它們採用二進制格式的編碼 ,其中HTTP1.x的首部信息會被封裝到Headers幀,而我們的request body則封裝到Data幀裏面。
-
這樣分幀以後這些幀就可以亂序發送,然後根據每個幀首部的流標識符號進行組裝
-
對比http/1.1因爲是基於文本以換行符分割每一條key:value則會有以下問題:
-
- 一次只能處理一個請求或者響應,因爲這種以分隔符分割消息的數據,在完成之前不能停止解析
- 解析這種數據無法預知需要多少內存,會給服務端有很大壓力
4,. 服務器主動推送資源
- 由於支持服務器主動推送資源,則可以省去一部分請求。比如你需要兩個文件1.html,1.css,如果是http1.0則需要請求兩次,服務端返回兩次。但是http2.0則可以客戶端請求一次,然後服務端直接回吐兩次
protobuf 使用
上面我們也說protobuf
的各種優勢,但其實他在我們實際應用到底怎麼用呢?
微服務架構中,由於每個服務對應的代碼庫是獨立運行的,無法直接調用,彼此間的通信就是個大問題.
gRPC可以實現將大的項目拆分爲多個小且獨立的業務模塊,也就是服務。各服務間使用高效的protobuf協議進行RPC調用,gRPC默認使用protocol buffers,這是google開源的一套成熟的結構數據序列化機制
可以用proto files創建gRPC服務,用message類型來定義方法參數和返回類型,相當於提前定義好接口,並且這個接口一但更改,客戶端和服務端都要改
當然也可以使用其他數據格式如JSON,甚至不用gRpc 直接用http 也可以,之所以用gRpc和protobuf是因爲他的這些優點
當然如果服務不是特別多,直接http 反而更爲方便
protoc 是什麼
protoc是 protocol compiler,是protocol 編譯器。
當我們從 https://github.com/protocolbu...官網安裝protobuf後,在命令行,就會有一個 protoc shell腳本命令。
目前 protoc 編譯器支持的語言有以下幾種。
我們使用protoc 命令行可以把.proto 文件轉譯成各種編程語言對應的源碼,包含數據類型定義、調用接口等。
protoc 命令把.proto文件轉成go的過程有兩步
- 解析.proto文件,轉譯成protobuf的原生數據結構在內存中保存;
- 把protobuf相關的數據結構傳遞給相應語言的編譯插件,由插件負責根據接收到的protobuf原生結構渲染輸出特定語言的模板。(對應go的protoc-gen-go)
安裝
到github 地址,下載即可,這裏使用win64
https://github.com/protocolbuffers/protobuf/releases
下載解壓後如下圖
將bin
目錄添加到環境變量中,使其可以全局使用
測試
隨便那個路徑開個CMD
, 輸入
protoc
可以看到如下圖
同時也有protoc
編譯工具的各個選項含義
protoc-gen-go
在官方文檔裏,
https://pkg.go.dev/github.com...裏說明到
protoc-gen-go 是 Google protobuf編譯器生成 Go 代碼的插件。
因爲protoc編譯器,默認沒有包含go語言代碼生成器,所以需要單獨安裝插件
安裝後會在GOPATH
目錄下生成可執行文件,protobuf的編譯器插件protoc-gen-go
,等下執行protoc
命令會自動調用這個插件
安裝
執行命令
go get -u github.com/golang/protobuf/protoc-gen-go
會自動下載到,你的$GOPATH/pkg/mod
裏,自動在go.mod
文件中添加依賴
proto 語法
詳細參考:
https://developers.google.cn/protocol-buffers/docs/proto3?hl=zh-cn
https://segmentfault.com/a/1190000007917576
https://developers.google.com/protocol-buffers/docs/gotutorial
hello World
寫一個最基本的hello world gRpc 服務,
創建項目的基本步驟
# 創建項目目錄
mkdir gRpc
# 切換到項目目錄
cd gRpc
# 創建RPC協議目錄
mkdir proto
# 初始化go模塊配置,用來管理第三方依賴, 本例子,項目模塊名是:gRpc
go mod init gRpc
# 添加依賴
go get -u github.com/golang/protobuf/protoc-gen-go
由於之前已經已經下載過,所有這個命令只會在go.mod
文件中增加依賴
此時的目錄結構
定義服務接口
定義服務,其實就是通過protobuf
語法定義語言平臺無關的接口。
文件:gRpc/proto/helloworld.proto
// 指定的當前proto語法的版本,有2和3,這裏用 3
// 這個必須在第一行
syntax = "proto3";
// 指定包名和目錄
// 這種不會 在 xxx.pb.go 文件插入包名,並且目錄是當前目錄不會創建文件夾
// 需要自己修改包名
// option go_package = "./";
// 這種會 在 xxx.pb.go 文件插入包名,並且會創建helloworld 文件夾
option go_package = "./gRpc";
// 定義Greeter服務
service Greeter {
// 定義SayHello方法,接受HelloRequest消息, 並返回HelloResponse消息
rpc SayHello (HelloRequest) returns (HelloResponse) {};
}
// 定義HelloRequest消息
message HelloRequest {
// name字段
string name = 1;
}
// 定義HelloResponse消息
message HelloResponse {
// message字段
string message = 1;
}
Greeter服務提供了一個SayHello接口,請求SayHello接口,
需要傳遞一個包含name字段的HelloRequest消息,
返回包含message字段的HelloResponse消息。
編譯proto協議文件生成 xxx.pb.go 文件
上面通過proto定義的接口,沒法直接在代碼中使用,因此需要通過protoc編譯器,將proto協議文件,編譯成go語言代碼。
# 切換到helloworld項目根目錄,執行命令
protoc -I proto/ --go_out=plugins=grpc:proto proto/helloworld.proto
如果出現問題:參考文章最後的解決辦法
protoc命令參數說明:
- -I 指定代碼輸出目錄,忽略服務定義的包名,否則會根據包名創建目錄
- --go_out 指定代碼輸出目錄,格式:--go_out=plugins=grpc:目錄名
- 命令最後面的參數是proto協議文件
編譯成功後在proto目錄生成了/helloworld/helloworld.pb.go文件,裏面包含了,我們的服務和接口定義。
添加依賴
可以看到上面有很多依賴錯誤,使用
go mod tidy
更新下依賴
實現服務端代碼並啓動
文件: server.go
package main
import (
"log"
"net"
"golang.org/x/net/context"
// 導入grpc包
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
// 導入剛纔我們生成的代碼所在的proto包。
pb "gRpc/proto/gRpc"
)
// 定義server,用來實現proto文件,裏面實現的Greeter服務裏面的接口
type server struct{}
// 實現SayHello接口
// 第一個參數是上下文參數,所有接口默認都要必填
// 第二個參數是我們定義的HelloRequest消息
// 返回值是我們定義的HelloResponse消息,error返回值也是必須的。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// 創建一個HelloResponse消息,設置Message字段,然後直接返回。
return &pb.HelloResponse{Message: "Hello " + in.Name}, nil
}
func main() {
// 監聽127.0.0.1:50051地址
lis, err := net.Listen("tcp", "127.0.0.1:50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 實例化grpc服務端
s := grpc.NewServer()
// 註冊Greeter服務
pb.RegisterGreeterServer(s, &server{})
// 往grpc服務端註冊反射服務
reflection.Register(s)
// 啓動grpc服務
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
啓動服務端
# 切換到項目根目錄,運行命令
go run server.go
實現客戶端代碼
文件:client.go
package main
import (
"log"
"time"
"golang.org/x/net/context"
// 導入grpc包
"google.golang.org/grpc"
// 導入剛纔我們生成的代碼所在的proto包。
pb "gRpc/proto/gRpc"
)
const (
defaultName = "makalo"
)
func main() {
// 連接grpc服務器
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
// 延遲關閉連接
defer conn.Close()
// 初始化Greeter服務客戶端
c := pb.NewGreeterClient(conn)
// 初始化上下文,設置請求超時時間爲1秒
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
// 延遲關閉請求會話
defer cancel()
// 調用SayHello接口,發送一條消息
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "makalo"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
// 打印服務的返回的消息
log.Printf("Greeting: %s", r.Message)
}
運行
# 切換到項目根目錄,運行命令
go run client.go
運行結果,至此我們已經完成 rpc 通信
目錄結構
PS F:\go\src\gRpc> tree /F
文件夾 PATH 列表
卷序列號爲 8DFC-EB47
F:.
│ client.go 客戶端代碼
│ go.mod go mod 文件
│ go.sum go mod 依賴關係文件
│ server.go 服務端代碼
│
└─proto
│ helloworld.proto rpc協議文件
│
└─gRpc
helloworld.pb.go rpc協議go版本的代碼
中間出現的錯誤
'protoc-gen-go' 不是內部或外部命令,也不是可運行的程序
protoc -I proto/ --go_out=plugins=grpc:proto proto/helloworld.proto
去看看你的 $GOPATH/bin
有沒有 protoc-gen-go.exe
,文件如果沒有,就說雖然它下載了代碼庫但是,並沒有編譯
我們上面添加依賴的鏈接是
github.com/golang/protobuf/protoc-gen-go
我們到 $GOPATH/pkg/mod
,按照上面目錄依次往下找即可,找到源碼庫
找到之後進入這代碼庫,重新編譯生成可執行文件名稱爲protoc-gen-go.exe
go build -o protoc-gen-go.exe main.go
將這個可執行文件剪切到$GOPATH/bin
(其他目錄也可以,只要能全局調用,只不過,這個目錄我們一開始最開始搭環境的時候就配置了環境變量)
vscode 中無法識別 protoc 命令
重啓vscode 終端
protoc-gen-go: unable to determine Go import path for "helloworld.proto"
protoc-gen-go: unable to determine Go import path for "helloworld.proto"
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
--go_out: protoc-gen-go: Plugin failed with status code 1.
需要指定 go_package
參數
option go_package = "./helloworld";
參考:
https://developers.google.cn/protocol-buffers/docs/gotutorial?hl=zh-cn