Golang語言之gRPC程式設計示例

尹正杰發表於2024-08-08

                                              作者:尹正傑

版權宣告:原創作品,謝絕轉載!否則將追究法律責任。

目錄
  • 一.RPC協議介紹
    • 1.什麼是RPC
    • 2.什麼是GRPC
    • 3.安裝gRPC環境
      • 3.1 使用gRPC的前提
      • 3.2 安裝protoc
      • 3.3 安裝go plugin
  • 二.Protocol Buffer的使用指南
    • 1.使用Protocol Buffer的基本流程概述
    • 2.編寫產品服務等proto檔案示例
    • 3.使用protoc工具編譯生成go程式碼
  • 三.基於gRPC的服務間通訊示例
    • 1.gRPC程式架構設計說明
    • 2.編寫產品服務
      • 2.1 編寫proto檔案並編譯go程式碼
      • 2.2 生成go.mod檔案並匯入依賴包
      • 2.3 編寫gRPC服務端程式碼
      • 2.4 啟動服務端測試
    • 3.編寫訂單服務
      • 3.1 編寫proto檔案並編譯go程式碼
      • 3.2 生成go.mod檔案並匯入依賴包
      • 3.3 編寫http服務端程式碼
      • 3.4 啟動服務端測試

一.RPC協議介紹

1.什麼是RPC

RPC對應的英文名稱為"Remote Procedure Call",即遠端過程呼叫。

與HTTP一致,也是應用程協議,該協議的目標是實現: 呼叫遠端過程(方法,函式)就如呼叫本地方法一致。
		
RPC協議沒有對網路層作出規範,那也就意味著具體的RPC實現可以基於TCP,也可以基於其他協議,例如:HTTP,UDP等。

RPC也沒有對資料傳輸格式做規範,也就是邏輯層面,傳輸JSON,Text,protobuf都可以。

廣泛使用的RPC產品有gRPC,Thrift等。說白了,RPC是一協議,GRPC,Thrift是實現RPC的具體的一個產品。

如上圖所示,RPC呼叫過程說明如下:
	- 1.ServiceA需要呼叫ServerB的FuncOnB函式,對於ServerA來說FuncOnB就是遠端過程;
	- 2.RPC的目的是讓ServiceA可以像呼叫ServiceA本地的函式一樣呼叫遠端函式FuncOnB;
			換句話說,也就是ServerA上程式碼層面上使用"serviceB.FuncOnB()"即可完成呼叫。
	- 3.RPC是C/S模式,呼叫方為Client,遠端方為Server;
	- 4.RPC把整個的呼叫過程,資料打包,網路請求等封裝完畢,在C,S兩端的stub(程式碼存根,就好像電影院檢票員會撕掉票的一部分留存證據是否有這張票)中;
	- 5.RPC典型呼叫流程如下:
		- 5.1 ServerA將呼叫需求告知Client Sub;
		- 5.2 Client Sub將呼叫目標(Call ID),引數資料(params)等呼叫資訊進行打包(序列化),並將打包好的呼叫資訊透過網路傳輸給Server Sub;
		- 5.3 Server Sub將根據呼叫資訊,呼叫相應過程,期涉及到資料的拆包(反序列化)等操作;
		- 5.4 遠端過程FuncOnB執行,並得到結果,將結果告知Server Sub;
		- 5.5 Server Sub將結果打包,並傳輸回給Client Sub;
		- 5.6 Client Sub將結果拆包,把最終函式呼叫的結果告知ServerA;  

2.什麼是GRPC

gRPC是一個高效能,開源的通用RPC框架。是一個Google開源的高效能遠端過程呼叫RPC框架,可以在任何環境中執行。

gRPC可以透過對負載平衡,跟蹤,健康檢查和身份驗證的可插拔支援有效的連線資料中心內和跨資料中心的服務。

gRPC也是用於分散式計算的最後一步,將裝置,移動應用程式和瀏覽器與後端服務對接。

gRPC支援多種主流程式語言,包括但不限於: C/C++,C#,Dart,Go,Java,Kotlin,Node.js,Objective-C,PHP,Python,Ruby等。

gRPC的核心是幫助咱們通訊,幫助我們去標準化,結構化訊息,最終的業務邏輯和gRPC無關。

gRPC官網地址:
	https://grpc.io 

3.安裝gRPC環境

3.1 使用gRPC的前提

使用Google的gRPC的前提條件是:
	- 1.安裝Golang環境;
	- 2.安裝Protocol Buffer編譯器protoc,推薦3版本;
	- 3.安裝go plugin,用於protocol buffer編譯器轉換成Golang環境的程式碼;
	
	
安裝protoc的方式:
	可以使用yum或apt包管理器安裝,但通常版本回比較滯後,因此更建議使用預編譯的二進位制方式安裝。
	protoc下載地址:
		https://github.com/protocolbuffers/protobuf

3.2 安裝protoc

Mac安裝protoc:
	1.下載protoc程式包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-osx-x86_64.zip

	2.解壓protoc程式
sudo unzip protoc-27.3-osx-x86_64.zip -d /usr/local/

	3.檢視安裝版本,如上圖所示
protoc --version


Linux安裝protoc:
	1.下載protoc程式包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-linux-x86_64.zip


	2.解壓protoc程式
sudo unzip protoc-27.3-linux-x86_64.zip -d /usr/local

	3.檢視安裝版本
protoc --version




windows安裝protoc:
	1.下載protoc程式包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-win64.zip

	2.解壓protoc程式
sudo unzip protoc-27.3-win64.zip -d <解壓到你自定義的安裝目錄即可,並將該目錄新增到PATH環境變數>

	3.檢視安裝版本
protoc --version 

3.3 安裝go plugin

	1.安裝protoc生成go程式碼的外掛包,程式會預設回直接安裝到"$GOPATH/bin"路徑下
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

	2.安裝protoc生成grpc的外掛包,程式會預設回直接安裝到"$GOPATH/bin"路徑下
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

	3.檢視grpc版本,如上圖所示
protoc-gen-go-grpc -version
protoc-gen-go --version

二.Protocol Buffer的使用指南

1.使用Protocol Buffer的基本流程概述

預設情況下,gRPC使用Protocol Buffers,這是Google用於序列化資料的成熟開源機制(儘管它可以與JSON等其他資料格式一起使用)。

使用Protocol Buffer的基本流程:
	1.使用protocol buffers語法定義訊息,訊息是用於傳遞的資料;
	2.使用protocol buffers與法定義服務,服務是RPC方法的集合,來使用訊息;
	3.使用protocol buffer編譯工具protoc來編譯,生成對應語言的程式碼,例如Go的程式碼;
	
使用Protocol Buffers的第一步是在".proto"檔案中定義序列化資料的結構,".proto"檔案是普通的文字檔案。

Protocol Buffer資料被結構化為訊息,其中每條訊息都是一個小的資訊邏輯記錄,包含一系列稱為欄位的key-value對。

除了核心內容外,".proto"檔案還需要指定與法版本,目前主流的也是最新的proto3版本,在".proto"檔案的開頭指定。

Protol Buffers官方文件:
	https://developers.google.com/protocol-buffers/docs/overview

2.編寫產品服務等proto檔案示例

syntax = "proto3";

// go_package 定義"go_package"屬性選項,protoc會基於包構建目錄,用來說明生成go程式碼所在的包。
// 名稱可以自定義,但該包的檔案儘量不要去修改。
option go_package = "./proto-compiles-readonly";



// ProductResponse 定義用於在服務間傳遞訊息,響應的產品資訊結構。
message ProductResponse {
    // 訊息的欄位,將來響應時會返回id,name和is_sale這三個欄位。
    int64 id = 1;
    string name = 2;
    bool is_sale = 3;
}


// ProductRequest 定義用於在服務間傳遞訊息,請求產品資訊時的引數訊息
message ProductRequest {
    // 將來基於id欄位來查詢資料。
    int64 id = 1;
}


// Product 定義服務可以完成的操作,這些方法需要在服務端實現,以便於客戶端呼叫時能夠響應。
service Product {
    // ProductInfo 是一個rpc方法,請求引數型別是ProductRequest,響應引數型別為ProductResponse
    rpc ProductInfo (ProductRequest) returns (ProductResponse) {}

    // 可以繼續定義其他的服務操作,gRPC的核心是幫助咱們通訊,幫助我們去標準化,結構化訊息,最終的業務邏輯和gRPC無關。
}

3.使用protoc工具編譯生成go程式碼

localhost:grpc yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product

./01-product:
product.proto
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ protoc --go_out=./01-product/ --go-grpc_out=./01-product/ ./01-product/product.proto 
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product

./01-product:
product.proto           proto-compiles-readonly

./01-product/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:grpc yinzhengjie$ 


溫馨提示:
	- 1.protoc相關引數說明:
		--go_out
			會自動生成一個名為"*pb.go"檔案,包含訊息型別的定義和操作相關程式碼。
		
		--go-grpc_out
			會自動生成一個名為"*_grpc.pb.go"檔案,包含客戶端和服務端的相關程式碼。
			
	- 2.生成的程式碼主要是結構上的封裝,在繼續使用時,還需要繼續充實業務邏輯;
	- 3.protoc生成的程式碼一般情況下不需要修改,如果的確有修改的需求,則需要重新結構體,重寫對應的方法即可;
	- 4.如果修改了proto檔案,則需要重新編譯生成對應心得go程式碼;

三.基於gRPC的服務間通訊示例

1.gRPC程式架構設計說明

定義兩個服務,訂單服務和產品服務,要求如下:
	- 1.訂單服務提供HTTP介面,用於完成訂單查詢,訂單中包含產品資訊,要利用grpc從產品服務獲取產品資訊;
	- 2.產品服務提供grpc介面,用於響應微服務內部產品資訊查詢;
	
如上圖所示,對於grpc來說,產品服務為服務端,訂單服務為客戶端,而訂單服務需要給使用者提供http介面。

同時不考慮其他業務邏輯,例如產品服務也需要對外提供http介面等,僅在乎grpc等通訊示例,同時也不考慮服務發現和閘道器等功能。

2.編寫產品服務

2.1 編寫proto檔案並編譯go程式碼

略,此過程直接參考上面的案例即可。我的目錄組織結構如上圖所示。

說白了,就是基於之前的案例繼續操作即可。

2.2 生成go.mod檔案並匯入依賴包

	1.生成go.mod檔案
localhost:01-product yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc/01-product
localhost:01-product yinzhengjie$
localhost:01-product yinzhengjie$ go mod init yinzhengjie-grpc
go: creating new go.mod: module yinzhengjie-grpc
go: to add module requirements and sums:
        go mod tidy
localhost:01-product yinzhengjie$ 
localhost:01-product yinzhengjie$ ls -R
go.mod                  product.proto           proto-compiles-readonly

./proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:01-product yinzhengjie$  

	2.匯入依賴包
localhost:01-product yinzhengjie$ go mod tidy

2.3 編寫gRPC服務端程式碼

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net"
	proto_compiles_readonly "yinzhengjie-grpc/proto-compiles-readonly"

	"google.golang.org/grpc"
)

var (
	port = flag.Int("port", 9150, "The gRPC Server Port")
)

// ProductServer 對rpc生成Go程式碼的方法進行重寫,實現完整的業務邏輯。
type ProductServer struct {
	proto_compiles_readonly.UnimplementedProductServer
}

// ProductInfo 對rpc生成Go程式碼的方法進行重寫,實現完整的業務邏輯。
func (ProductServer) ProductInfo(context.Context, *proto_compiles_readonly.ProductRequest) (*proto_compiles_readonly.ProductResponse, error) {
	// 假設data是我們查詢到資料
	data := proto_compiles_readonly.ProductResponse{
		Id:     18,
		Name:   "尹正傑 Go K8S 雲原生,部落格地址: https://www.cnblogs.com/yinzhengjie",
		IsSale: true,
	}

	// 返回查詢到的資料
	return &data, nil
}

func main() {
	flag.Parse()

	// 設定TCP的監聽地址
	listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port))
	if err != nil {
		log.Fatalln(err)
	}

	// 新建(例項化)grpc的伺服器
	gRpcServer := grpc.NewServer()

	// 將咱們重寫後的產品服務(ProductServer)註冊到grpc服務端。
	proto_compiles_readonly.RegisterProductServer(gRpcServer, &ProductServer{})

	// 啟動監聽服務
	log.Printf("gRPC Server is listening on %s.\n", listener.Addr())
	if err := gRpcServer.Serve(listener); err != nil {
		log.Fatalf("err = %v\n", err)
	}

}

2.4 啟動服務端測試

如上圖所示,啟動產品服務的服務端執行測試,如果能夠監聽地址成功說明是沒問題的喲~

localhost:01-product yinzhengjie$ go run grpcService.go 
2024/08/09 00:01:05 gRPC Server is listening on 127.0.0.1:9150.

3.編寫訂單服務

3.1 編寫proto檔案並編譯go程式碼

略,此步驟和編寫產品的服務的步驟一致,因為gRPC呼叫時服務端和客戶端都需要有proto的程式碼。

localhost:grpc yinzhengjie$ protoc --go_out=./02-order/ --go-grpc_out=./02-order/ ./02-order/product.proto 
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product      02-order

./01-product:
go.mod                  go.sum                  grpcService.go          product.proto           proto-compiles-readonly

./01-product/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go

./02-order:
product.proto           proto-compiles-readonly

./02-order/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:grpc yinzhengjie$ 

3.2 生成go.mod檔案並匯入依賴包

	1.生成go.mod檔案
localhost:02-order yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc/02-order
localhost:02-order yinzhengjie$ 
localhost:02-order yinzhengjie$ go mod init yinzhengjie-order
go: creating new go.mod: module yinzhengjie-order
go: to add module requirements and sums:
        go mod tidy
localhost:02-order yinzhengjie$ 
localhost:02-order yinzhengjie$ ls -R
go.mod                  product.proto           proto-compiles-readonly

./proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:02-order yinzhengjie$ 

	2.匯入依賴包
localhost:02-order yinzhengjie$ go mod tidy

3.3 編寫http服務端程式碼

package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"time"
	proto_compiles_readonly "yinzhengjie-order/proto-compiles-readonly"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var (
	gRPCAddr = flag.String("grpc", "127.0.0.1:9150", "gRPC Server Address")
	addr     = flag.String("addr", "127.0.0.1", "web server's listen Address. Default: 127.0.0.1")
	port     = flag.Int("port", 8080, "web server's listen Port. Default: 8080 ")
)

// OrderHandle 完成gRPC的客戶端請求
func OrderHandle(write http.ResponseWriter, request *http.Request) {

	// 1.連線到gRPC服務端
	conn, err := grpc.Dial(*gRPCAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	// grpc.NewClient()
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

	// 2.例項化gRPC客戶端
	client := proto_compiles_readonly.NewProductClient(conn)

	// 3.定義上下文環境,超時機制,若超過3秒就認為超時,將來取消呼叫
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	// 4.開始遠端過程呼叫,客戶端攜帶當前上下文環境以及請求到資料型別
	// 注意:ProductInfo的業務邏輯是在product服務端定義的,客戶端只是呼叫而已。
	response, err := client.ProductInfo(ctx, &proto_compiles_readonly.ProductRequest{
		Id: 18,
	})
	if err != nil {
		log.Fatalln(err)
	}

	// 5.構建http的響應
	data := struct {
		ID       int64                                      `json:"id"`
		Auther   string                                     `json:"Auther`
		Products []*proto_compiles_readonly.ProductResponse `json:"products"`
	}{
		1001, "Jason Yin", []*proto_compiles_readonly.ProductResponse{
			response,
		},
	}

	dataJson, err := json.Marshal(data)
	if err != nil {
		log.Fatalln(err)
	}

	// 6.響應http的客戶端
	write.Header().Set("Context-Type", "application/json")

	if _, err := fmt.Fprintf(write, string(dataJson)); err != nil {
		log.Fatalln(err)
	}
}

func main() {

	// 定義業務邏輯服務,假設為訂單服務
	service := http.NewServeMux()

	// 當使用者訪問訂單服務時,再去觸發呼叫gRPC客戶端請求
	service.HandleFunc("/order", OrderHandle)

	// 啟動httpServer監聽
	address := fmt.Sprintf("%s:%d", *addr, *port)
	fmt.Printf("訂單服務(Order Service) 監聽地址: %s\n", address)
	log.Fatalln(http.ListenAndServe(address, service))
}

3.4 啟動服務端測試

	1.啟動服務
localhost:02-order yinzhengjie$ go run httpServer.go 
訂單服務(Order Service) 監聽地址: 127.0.0.1:8080

	2.訪問測試
如上圖所示。

相關文章