1. 程式人生 > >gRPC-go 入門(1):Hello World

gRPC-go 入門(1):Hello World

## 摘要 在這篇文章中,主要是跟你介紹一下`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等作為客戶端。為了實現這一點,我們在”定義服務“和在編碼和解碼的過程中,應該是做到**語言無關的**。 下面放一張官網上面的圖: ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161632078-309009747.jpg) 因此,`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的兩個檔案: ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161817553-1294594791.jpg) 至此,所有的準備工作已經完成。 ## 3. proto檔案建立 在開始開發之前,先說說我們的目標。 在這個`grpc-practice`專案中,我希望實現一個功能,客戶端可以傳送訊息給服務端,服務端收到訊息後,返回響應給客戶端。 正如前面所說的,在開發`server`與`client`之前,我們需要先定義服務。 因此,在這一節的內容中,我將向你介紹proto檔案的編寫。 ### 3.1 專案結構 在這之前,先讓我們看看整個專案的初始結構。 ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161834930-1252892090.jpg) `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 ``` 這兩條命令會生成如下的兩個檔案: ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161855325-187405311.jpg) 在這兩個檔案中,包含了我們定義方法的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`端都跑起來,你會看到這樣的畫面: ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162448389-804698371.jpg) ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162500188-1337762987.jpg) 至此,成功Hello了個World。 ## 寫在最後 首先,謝謝你能看到這裡! 在這篇文章中,主要是跟你介紹一下hello world的寫法,以及在say hello的過程中可能遇到的一些坑。 我認為最大的坑是在於`protoc-gen-go`這個外掛這裡,因為兩種語法讓我迷惑了很久。 如果在這期間,你還有一些問題沒有解決,歡迎留言,或者直接公眾號找到我,我們一起研究。 如果在文章中有哪些錯誤,還請不吝指教,謝謝! 最後,再次感謝你能看到這裡! 按照慣例,甩個公眾號在這,不管有沒有問題,都歡迎來找我玩~ ![](https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162007452-17827310