摘要
在這篇文章中,主要是跟你介紹一下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
這個外掛這裡,因為兩種語法讓我迷惑了很久。
如果在這期間,你還有一些問題沒有解決,歡迎留言,或者直接公眾號找到我,我們一起研究。
如果在文章中有哪些錯誤,還請不吝指教,謝謝!
最後,再次感謝你能看到這裡!
按照慣例,甩個公眾號在這,不管有沒有問題,都歡迎來找我玩~