摘要
在這篇文章中,主要是跟你介紹一下gRPC
這個東西。
然后,我會創建一個簡單的練習項目,作為gRPC
的Hello World項目。
在這個項目中,只有很簡單的一個RPC函數,用於說明gRPC
的工作方式。
此外,我也會跟你分享一下我初次接觸gRPC
所遇到的一些坑,主要是在protocol buffer
的proto-gen-go
插件上面。
1. 簡單介紹
在這一節的內容中,我將簡單的跟你介紹一下gRPC
這個東西。
RPC
的全稱是Remote Procedure Call
,遠程過程調用。這是一種協議,是用來屏蔽分布式計算中的各種調用細節,使得你可以像是本地調用一樣直接調用一個遠程的函數。
而gRPC
又是什么呢?用官方的話來說:
A high-performance, open-source universal RPC framework
gRPC
是一個高性能的、開源的通用的RPC框架。
在gRPC
中,我們稱調用方為client
,被調用方為server
。
跟其他的RPC
框架一樣,gRPC
也是基於”服務定義“的思想。簡單的來講,就是我們通過某種方式來描述一個服務,這種描述方式是語言無關的。在這個”服務定義“的過程中,我們描述了我們提供的服務服務名是什么,有哪些方法可以被調用,這些方法有什么樣的入參,有什么樣的回參。
也就是說,在定義好了這些服務、這些方法之后,gRPC
會屏蔽底層的細節,client
只需要直接調用定義好的方法,就能拿到預期的返回結果。對於server
端來說,還需要實現我們定義的方法。同樣的,gRPC
也會幫我們屏蔽底層的細節,我們只需要實現所定義的方法的具體邏輯即可。
你可以發現,在上面的描述過程中,所謂的”服務定義“,就跟定義接口的語義是很接近的。我更願意理解為這是一種”約定“,雙方約定好接口,然后server
實現這個接口,client
調用這個接口的代理對象。至於其他的細節,交給gRPC
。
此外,gRPC
還是語言無關的。你可以用C++作為服務端,使用Golang、Java等作為客戶端。為了實現這一點,我們在”定義服務“和在編碼和解碼的過程中,應該是做到語言無關的。
下面放一張官網上面的圖:
因此,gRPC
使用了Protocol Buffers
。
在這里我不會展開來講Protocol Buffers
這個東西,你可以把他當成一個代碼生成工具以及序列化工具。這個工具可以把我們定義的方法,轉換成特定語言的代碼。比如你定義了一種類型的參數,他會幫你轉換成Golang
中的struct 結構體
,你定義的方法,他會幫你轉換成func 函數
。此外,在發送請求和接受響應的時候,這個工具還會完成對應的編碼和解碼工作,將你即將發送的數據編碼成gRPC
能夠傳輸的形式,又或者將即將接收到的數據解碼為編程語言能夠理解的數據格式。
對gRPC
的簡單介紹就到這里,下面的內容我們直接開始實踐。
2. 環境配置
在這一節中,可能很多內容會不那么的適用。
但是限於篇幅,我沒有列舉所有的安裝方式。如果在安裝的過程中你遇到了問題,可以在網上搜索解決,也可以在文章末尾找到我的聯系方式,我們一起研究。
2.1 gRPC
go get google.golang.org/grpc
這一步安裝的是gRPC
的核心庫,但是這一步是需要(特別的上網方式)的。所以如果在安裝過程中出錯了,你可以科學一波,也可以找一找其他的安裝方法。
2.2 protocol buffers
在Mac OS中,直接用brew安裝。
brew info protobuf
2.3 protoc-gen-go
上一步安裝的是protocol編譯器。而上文中我們提到了可以生成各種不同語言的代碼。因此,除了這個編譯器,我們還需要配合各個語言的代碼生成工具。
對於Golang
來說,稱為protoc-gen-go
。
不過在這兒有個小小的坑,github.com/golang/protobuf/protoc-gen-go
和google.golang.org/protobuf/cmd/protoc-gen-go
是不同的。
區別在於前者是舊版本,后者是google接管后的新版本,他們之間的API是不同的,也就是說用於生成的命令,以及生成的文件都是不一樣的。
因為目前的gRPC-go
源碼中的example用的是后者的生成方式,為了與時俱進,本文也采取最新的方式。
你需要安裝兩個庫:
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
因為這些文件在安裝grpc
的時候,已經下載下來了,因此使用install
命令就可以了,而不需要使用get
命令。
然后你看你的$GOPATH路徑,應該有標1和2的兩個文件:
至此,所有的准備工作已經完成。
3. proto文件創建
在開始開發之前,先說說我們的目標。
在這個grpc-practice
項目中,我希望實現一個功能,客戶端可以發送消息給服務端,服務端收到消息后,返回響應給客戶端。
正如前面所說的,在開發server
與client
之前,我們需要先定義服務。
因此,在這一節的內容中,我將向你介紹proto文件的編寫。
3.1 項目結構
在這之前,先讓我們看看整個項目的初始結構。
server
和client
我們先不管,在這一節內容中我們先編寫`*.proto'文件。
在proto文件夾中創建message.proto
文件。
在文件的第一行,我們寫上:
syntax = "proto3";
這是在說明我們使用的是proto3
語法。
然后我們應該寫上:
option go_package = ".;message";
這部分的內容是關於最后生成的go文件是處在哪個目錄哪個包中,.
代表在當前目錄生成,message
代表了生成的go文件
的包名是message
。
然后我們需要定義一個服務,在這個服務中需要有一個方法,這個方法可以接受客戶端的參數,再返回服務端的響應。
那么我們可以這么寫:
service MessageSender {
rpc Send(MessageRequest) returns (MessageResponse) {}
}
其實很容易可以看出,我們定義了一個service,稱為MessageSender
,這個服務中有一個rpc方法,名為Send
。這個方法會發送一個MessageRequest
,然后返回一個MessageResponse
。
讓我們在看看具體的MessageRequest
和MessageResponse
:
message MessageResponse {
string responseSomething = 1;
}
message MessageRequest {
string saySomething = 1;
}
message
關鍵字,其實你可以理解為Golang
中的結構體。這里比較特別的是變量后面的“賦值”。注意,這里並不是賦值,而是在定義這個變量在這個message中的位置。更具體的內容我應該會在源碼分析部分講到。
在編寫完上面的內容后,在/grpc-practice/src/helloworld/proto
目錄下執行如下命令:
protoc --go_out=. message.proto
protoc --go-grpc_out=. message.proto
這兩條命令會生成如下的兩個文件:
在這兩個文件中,包含了我們定義方法的go語言實現,也包含了我們定義的請求與相應的go語言實現。
簡單來講,就是protoc-gen-go
已經把你定義的語言無關的message.proto
轉換為了go語言的代碼,以便server
和client
直接使用。
注意,到了這一部分你可能會有一些疑惑。
在網上的一些教程中,有這樣的生成方式:
protoc --go_out=plugins=grpc:. helloworld.proto
這種生成方式,使用的就是github
版本的protoc-gen-go
,而目前這個項目已經由Google接管了。
並且,如果使用這種生成方式的話,並不會生成上圖中的xxx_grpc.pb.go
與xxx.pb.go
兩個文件,只會生成xxx.pb.go
這種文件。
此外,你也可能遇到這種錯誤:
protoc-gen-go-grpc: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go-grpc_out: protoc-gen-go-grpc: Plugin failed with status code 1.
這是因為你沒有安裝protoc-gen-go-grpc
這個插件,這個問題在本文中應該不會出現。
你還可能會遇到這種問題:
--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
這是因為你安裝的是更新版本的protoc-gen-go
,但是你卻用了舊版本的生成命令。
但是這兩種方法都是可以完成目標的,只不過api
不太一樣。本文是基於Google版本的protoc
-gen-go進行示范。
至於其他更詳細的資料,你可以在這里看到:https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.20.0#v1.20-generated-code
4. 服務端
4.1 注冊
我們在server目錄下面創建一個server.go
文件。
在main函數中加入如下的代碼:
srv := grpc.NewServer()
message.RegisterMessageSenderService(srv, &message.MessageSenderService{})
很容易可以看出,我們在這一部分創建了一個Server,然后注冊了我們的Service。
在注冊函數的第二個參數中,我們傳進去了一個MessageSenderService
實例。
來看看這個實例有什么樣的結構:
type MessageSenderService struct {
Send func(context.Context, *MessageRequest) (*MessageResponse, error)
}
可以看出,這個實例里面有一個方法,這個方法就是我們定義的send方法。也就是說,這一部分是需要我們在Server
端實現這個send方法的。
因此我們創建這么一個方法:
func handleSendMessage(ctx context.Context, req *message.MessageRequest) (*message.MessageResponse, error) {
log.Println("receive message:", req.GetSaySomething())
resp := &message.MessageResponse{}
resp.ResponseSomething = "roger that!"
return resp, nil
}
注意,“實現定義的方法”,並不是說我們需要創建一個同名的方法,而是說我們需要創建一個有相同函數簽名的方法。也就是說,需要有相同的入參,出參。
然后我們將這個方法寫進注冊函數中,變成了這樣:
message.RegisterMessageSenderService(srv, &message.MessageSenderService{
Send: handleSendMessage,
})
至此,我們已經成功的在server
端實現了我們聲明的方法了。
4.2 監聽
其實這個過程跟golang的web服務器是很像的,也是創建Handler,然后對端口進行監聽。
那么到了這一步也一樣。
listener, err := net.Listen("tcp", ":12345")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = srv.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
監聽12345
端口的TCP連接,然后啟動服務器。
至此,服務端開發完畢。
5. 客戶端
在客戶端中,我們應該先與server
端建立連接,然后才能夠調用各種方法。
conn, err := grpc.Dial("127.0.0.1:12345", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
以上代碼,就是跟本地的12345
端口建立連接。
然后,按照定義,我們調用server
端的方法,應該要像調用本地方法一樣方便。
那么,我們這么做:
client := message.NewMessageSenderClient(conn)
resp, err := client.Send(context.Background(), &message.MessageRequest{SaySomething: "hello world!"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
很容易可以理解,我們在本地創建了一個client,然后直接調用我們之前定義好的Send方法,就可以實現我們需要的邏輯了。
簡單的來講,我們在*.proto
文件中定義了方法,然后在server
端實現定義的rpc方法的具體邏輯,在client端調用這個方法。
對於其他的部分,由proto buffer
負責對Golang
中存儲的數據結構與rpc
傳輸中的數據進行轉換,grpc
負責封裝所有的邏輯。
server
端和client
端都跑起來,你會看到這樣的畫面:
至此,成功Hello了個World。
寫在最后
首先,謝謝你能看到這里!
在這篇文章中,主要是跟你介紹一下hello world的寫法,以及在say hello的過程中可能遇到的一些坑。
我認為最大的坑是在於protoc-gen-go
這個插件這里,因為兩種語法讓我迷惑了很久。
如果在這期間,你還有一些問題沒有解決,歡迎留言,或者直接公眾號找到我,我們一起研究。
如果在文章中有哪些錯誤,還請不吝指教,謝謝!
最后,再次感謝你能看到這里!
按照慣例,甩個公眾號在這,不管有沒有問題,都歡迎來找我玩~