gRPC-go 入門(1):Hello World

紅雞菌發表於2020-09-24

摘要

在這篇文章中,主要是跟你介紹一下gRPC這個東西。

然後,我會建立一個簡單的練習專案,作為gRPC的Hello World專案。

在這個專案中,只有很簡單的一個RPC函式,用於說明gRPC的工作方式。

此外,我也會跟你分享一下我初次接觸gRPC所遇到的一些坑,主要是在protocol bufferproto-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-gogoogle.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專案中,我希望實現一個功能,客戶端可以傳送訊息給服務端,服務端收到訊息後,返回響應給客戶端。

正如前面所說的,在開發serverclient之前,我們需要先定義服務。

因此,在這一節的內容中,我將向你介紹proto檔案的編寫。

3.1 專案結構

在這之前,先讓我們看看整個專案的初始結構。

serverclient我們先不管,在這一節內容中我們先編寫`*.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

讓我們在看看具體的MessageRequestMessageResponse

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語言的程式碼,以便serverclient直接使用。

注意,到了這一部分你可能會有一些疑惑。

在網上的一些教程中,有這樣的生成方式:

protoc --go_out=plugins=grpc:. helloworld.proto

這種生成方式,使用的就是github版本的protoc-gen-go,而目前這個專案已經由Google接管了。

並且,如果使用這種生成方式的話,並不會生成上圖中的xxx_grpc.pb.goxxx.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這個外掛這裡,因為兩種語法讓我迷惑了很久。

如果在這期間,你還有一些問題沒有解決,歡迎留言,或者直接公眾號找到我,我們一起研究。

如果在文章中有哪些錯誤,還請不吝指教,謝謝!

最後,再次感謝你能看到這裡!

按照慣例,甩個公眾號在這,不管有沒有問題,都歡迎來找我玩~

相關文章