RPC 笔记(03)— gRPC 概念、安装、编译、客户端和服务端示例
1. gRPC 概念
gRPC 是 Google 開源的一款高性能的 RPC 框架。GitHub 上介紹如下:
gRPC is a modern, open source, high-performance remote procedure call (RPC) framework that can run anywhere.
市面上的 RPC 框架數不勝數,包括 Alibaba Dubbo 和微博的 Motan 等。gRPC 能夠在眾多的框架中脫穎而出,是跟其高性能是密切相關的。
?
1.1 接口設計
對一個遠程服務 Service 的調用,gRPC 約定 Client 和 Server 首先需要約定好 Service 的結構。包括一系列方法的組合,每個方法定義、參數、返回體等。對這個結構的描述,gRPC 默認是用 Protocol Buffer 去實現的。
1.2 Streaming
Streaming 在 HTTP/1.x 已經出現了,HTTP2 實現了 Streaming 的多路復用。gRPC 是基于 HTTP2 實現的。所以 gRPC 也實現了 Streaming 的多路復用,所以 gRPC 的請求有四種模式:
- Simple RPC
- Client-side Streaming RPC
- Server-side Streaming RPC
- Bidirectional Streaming RPC
也就是說同時支持單邊流和雙向流。
1.3 Protocol
gRPC 的協議層是基于 HTTP2 設計的,所以你如果想了解 gRPC 的話,可以先深入了解 HTTP2。
1.4 Flow Control
gRPC 的協議支持流量控制,這里也是采用了 HTTP2 的 Flow Control 機制。
通過上面的介紹可以看到,gRPC 的高性能很大程度上依賴了 HTTP2 的能力,所以要了解 gRPC 之前,我們需要先了解一下 HTTP 2 的特性。
?
1.5 HTTP2 特性
- 二進制協議
眾所周知,二進制協議比文本形式的協議,發送的數據量肯定是更小,傳輸效率更高的。所以 HTTP2 比 HTTP/1.x 更高效,因為二進制是不可讀的,所以會損失部分可讀性。
- 多路復用的流
HTTP/1.x 一個 Stream 是需要一個 TCP 連接的,其實從性能上來說是比較浪費的。HTTP2 可以復用 TCP 連接,實現一個 TCP 連接可以處理多個 Stream,同時可以為每一個 Stream 設置優先級,可以用來告訴對端哪個流更重要。當資源有限的時候,服務器會根據優先級來選擇應該先發送哪些流。
- 頭部壓縮
由于 HTTP 協議是一個無狀態的協議,導致了很多相同或者類似的 HTTP 請求重復發送時,帶的頭部信息很多時候是完全一樣的。HTTP2 對頭部進行壓縮,可以減少數據包大小,提高協議性能。
- 請求 Reset
在 HTTP/1.x 中,當一個含有確切值的 Content-Length 的 HTTP 消息發出之后,需要斷開 TCP 連接才能中斷。這樣會導致需要通過三次握手來重新建立一個新的 TCP 連接,代價是比較大的。在 HTTP2 里面,我們可以通過發送 RST_STREAM 幀來終止當前消息發送,從而避免了浪費帶寬和中斷已有的連接。
- 服務器推送
如果一個 Client 請求資源 A,而 Server 知道 Client 可能也會需要資源 B, 所以在 Client 發起請求前,Server 提前將 B 推送給 A 緩存起來,從而可以縮短資源 A 這個請求的響應時間。
- Flow Control
在 HTTP2 中,每個 HTTP Stream 都有自己公示的流量窗口,對于每個 Stream 來說,Client 和 Server 都必須互相告訴對方自己能夠處理的窗口大小,Stream 中的數據幀大小不得超過能處理的窗口值。
?
2. Protobuf 文件編寫
2.1 安裝 protoc 編譯器
將 Proto 協議文件轉換為多種語言對應格式的工具,根據對應平臺選擇對應的安裝包,安裝包下載地址https://github.com/protocolbuffers/protobuf/releases
cd ~/tmp
# 下載
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
# 解壓后得到 bin 目錄下的 protoc
unzip protoc-3.11.4-linux-x86_64.zip
# 創建存放 protoc 目錄
sudo mkdir /usr/local/protobuf
# 復制 protoc 到剛剛創建的目錄下
sudo cp bin/protoc /usr/local/protobuf/
添加 protoc 環境變量
vim /etc/profile
# 在文件末尾修改
PATH=$PATH:/usr/local/php/bin:/usr/local/protobuf
# 使其修改生效
source /etc/profile
查看是否安裝成功,
protoc --versionlibprotoc 3.11.4
2.2 編寫 Protocol Buffers 文件
無論使用何種語言創建客戶端和服務端,都依照相同的 Proto 文件定義的協議接口和數據格式,客戶端和服務器端都會使用由服務定義產生的接口代碼。
?
user.proto 文件
syntax = "proto3"; // 指定proto版本
package user_proto; // 指定包名// 定義 User 服務
service User {// 定義 GetUser 方法 - 獲取某個 user 數據rpc GetUser(GetUserRequest) returns (GetUserResponse) {}// 定義 GetUserList 方法 - 獲取 user 所有數據rpc GetUserList(GetUserListRequest) returns (UserListResponse) {}
}// 枚舉類型第一個字段必須為 0
enum UserSex {MEN = 0;WOMEN = 1;
}// GetUser 請求結構
message GetUserRequest {int64 userid = 1;
}// GetUser 響應結構
message GetUserResponse {int64 userid = 1;string username = 2;UserSex sex = 3;
}// GetUserList 請求結構
message GetUserListRequest {}// 響應結構
message UserListResponse {// repeated 重復(數組)repeated GetUserResponse list = 1;
}
2.3 Protobuf 文件生成于 Go 文件
要讓 user.proto 生成 Go 文件,需要 protoc-gen-go 所以要下載:
go get github.com/golang/protobuf/protoc-gen-go
在 bin 目錄下會生成一個 protoc-gen-go 可執行文件,就是用于生成 Go 文件的。
?
3. 編寫 Go gRPC 服務端流程
首先創建一個為 go-grpc 的項目:
mkdir ~/go-grpc
設置 Go 模塊代理,因為我們要使用 Go modules 第三方包的依賴管理工具,當然了你的 Go 環境最好是 1.13 以上。
go env -w GOPROXY=https://goproxy.io,direct
3.1 初始化這個項目
我們使用 Go modules 來初始化(創建)這個項目,畢竟是以后的主流了
cd ~/go-grpc
go mod init go-grpc
下載項目所使用的包,它們之間的依賴由 Go modules 幫我們完成了,記住一定要在項目下打開命令行下執行:
go get github.com/golang/protobuf
go get google.golang.org/grpc
創建 user_proto 目錄,將剛剛編寫的 user.proto 放進來:
go-grpc
├── go.mod
├── go.sum
└── user_proto└── user.proto
生成 Go 文件,這里用了 plugins 選項,提供對 gRPC 的支持,否則不會生成 Service 的接口,方便編寫服務器和客戶端程序:
cd ~/go-grpc/user_proto
protoc --go_out=plugins=grpc:. user.proto
根據編譯指令,編譯成對應語言的代碼文件:
protoc -I=$SRC_DIR --xxx_out=$DST_DIR $SRC_DIR/xxx.proto
$SRC_DIR:存放協議源文件的目錄地址;$DST_DIR:輸出代碼文件的目錄地址;xxx.proto:協議源文件名稱;–xxx_out:根據自己的需要,選擇對應的語言,例如(Java:–java_out,C++:–cpp_out等);- 可通過在命令提示符中輸入
protoc --help查看更多幫助。
查看目錄:
go-grpc
├── go.mod
├── go.sum
└── user_proto├── user.pb.go└── user.proto
3.2 創建服務端
讓 UserServer 服務工作有兩個部分:
- 實現我們服務定義的生成服務接口,做我們服務的實際工作
- 運行一個
gRPC服務器,監聽來自客戶端的請求并返回服務的響應
mkdir ~/go-grpc/server
cd ~/go-grpc/server
我們首先實現 user.pb.go 中的 UserServer,即我們服務的實際工作接口:
// UserServer is the server API for User service.
type UserServer interface {// 定義 GetUser 方法GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)// 定義 GetUserList 方法GetUserList(context.Context, *GetUserListRequest) (*UserListResponse, error)
}
創建 user.go 來實現 UserServer 接口,即我們實際的工作服務實現:
package main
import ("context""strconv"// 引入 proto 編譯生成的包pb "go-grpc/user_proto"
)// 定義 User 并實現約定的接口
type User struct {UserId int64 `json:"user_id"`UserName string `json:"user_name"`
}// 獲取某個 user 數據
func (this *User) GetUser(ctx context.Context, ut *pb.GetUserRequest) (*pb.GetUserResponse, error) {// 待返回數據結構resp := new(pb.GetUserResponse)resp.Userid = ut.Useridresp.Username = "laixhe"resp.Sex = pb.UserSex_MENreturn resp, nil
}// 獲取 user 所有數據
func (this *User) GetUserList(ctx context.Context, ut *pb.GetUserListRequest) (*pb.UserListResponse, error) {list := make([]*pb.GetUserResponse, 0, 3)for i := 1; i <= 3; i++ {list = append(list, &pb.GetUserResponse{Userid: int64(i), Username: "laiki"+strconv.Itoa(i), Sex: pb.UserSex_MEN})}// 待返回數據結構resp := new(pb.UserListResponse)resp.List = listreturn resp, nil
}
現在我們開始編寫對外服務 main.go,以便客戶端可以實際使用我們的服務:
- 創建監聽
listener - 創建
gRPC的服務 - 將我們的服務注冊到
gRPC的Server中 - 啟動
gRPC服務,將我們自定義的監聽信息傳遞給gRPC客戶端
package main
import ("log""net"// 引入 proto 編譯生成的包pb "go-grpc/user_proto""google.golang.org/grpc"
)func main() {// 監聽地址和端口listen, err := net.Listen("tcp", ":50051")if err != nil {log.Fatalf("監聽端口失敗: %v", err)}// 實例化 grpc ServerserverGrpc := grpc.NewServer()// 注冊 User servicepb.RegisterUserServer(serverGrpc, &User{})log.Println("開始監聽 Grpc 端口 0.0.0.0:50051")// 啟動服務err = serverGrpc.Serve(listen)if err != nil {log.Println("啟動 Grpc 服務失敗")}
}
查看目錄:
go-grpc
├── go.mod
├── go.sum
├── server
│ ├── main.go
│ └── user.go
└── user_proto├── user.pb.go└── user.proto
我們回顧下:
- 首先要實現
UserServer接口 - 創建
gRPC Server對外端口 - 注冊我們實現的
UserServer接口的實例 - 最后調用
Serve()啟動我們的服務
4. 編寫 Go gRPC 客戶端流程
首先創建我們所需的目錄:
mkdir ~/go-grpc/client
cd ~/go-grpc/client
4.1 初始化客戶端
首先在連接我們建立好的服務端的 IP 和端口 main.go,通過把服務器地址和端口號傳遞給 grpc.Dial() 來創建通道:
package main
import ("log""net/http"// 引入 proto 編譯生成的包pb "go-grpc/user_proto""google.golang.org/grpc"
)const (// Address gRPC 服務地址Address = "127.0.0.1:50051"
)var UClient pb.UserClient// 初始化 Grpc 客戶端
func initGrpc() {// 連接 GRPC 服務端conn, err := grpc.Dial(Address, grpc.WithInsecure())if err != nil {log.Fatalln(err)}// 初始化 User 客戶端UClient = pb.NewUserClient(conn)log.Println("初始化 Grpc 客戶端成功")
}// 啟動 http 服務
func main() {initGrpc()http.HandleFunc("/user/get", GetUser)http.HandleFunc("/user/list", GetUserList)log.Println("開始監聽 http 端口 0.0.0.0:8080")err := http.ListenAndServe(":8080", nil)if err != nil {log.Printf("http.ListenAndServe err:%v", err)}
}
對外 HTTP 的兩接口的實現 user.go:
- 創建
gRPC連接器 - 創建
gRPC客戶端,并將連接器賦值給客戶端 - 向
gRPC服務端發起請求 - 獲取
gRPC服務端返回的結果
package main
import ("context""encoding/json""net/http""strconv"// 引入 proto 編譯生成的包pb "go-grpc/user_proto"
)func GetUser(w http.ResponseWriter, r *http.Request) {// 獲取 GET 的參數userid := r.FormValue("userid")id, err := strconv.ParseInt(userid, 10, 0)if err != nil {w.Write([]byte("userid The parameters must be integers"))return}// 調用 Grpc 的遠程接口data, err := UClient.GetUser(context.Background(), &pb.GetUserRequest{Userid: id})if err != nil {w.Write([]byte("Grpc: " + err.Error()))return}// json 格式化js, _ := json.Marshal(data)w.Write(js)
}func GetUserList(w http.ResponseWriter, r *http.Request) {// 調用 Grpc 的遠程接口data, err := UClient.GetUserList(context.Background(), &pb.GetUserListRequest{})if err != nil {w.Write([]byte("Grpc: " + err.Error()))return}// json 格式化js, _ := json.Marshal(data.List)w.Write(js)
}
我們回顧下:
- 通于
grpc.Dial()連接服務端 - 持有某個
gRPC的服務連接NewUserClient
總結
以上是生活随笔為你收集整理的RPC 笔记(03)— gRPC 概念、安装、编译、客户端和服务端示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RPC 笔记(01)— RPC概念、调用
- 下一篇: 2022-2028年中国美瞳行业应用市场