iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
Modern Web

Go 快 Go 高效: 從基礎語法到現代Web應用開發系列 第 25

【Day25】即時串流通信服務 II | 在 Golang 中開發 gRPC 服務

  • 分享至 

  • xImage
  •  

前言

上篇我們介紹了 gRPC 的特性和運作原理,那今天就來介紹如何實際上手開發吧!

環境架設

本操作皆是在 Mac 上執行

  • gRPC 需要用到的 Protocol Buffers
brew install protobuf
  • 用於生成 Go 語言專用的 gRPC 和 Protobuf 程式碼的插件
brew install protoc-gen-go-grpc protoc-gen-go
  • 初始化專案的 Go module
go mod init <your-module-name>
  • 在我們專案的 terminal 下安裝需要用到的依賴包
go get -u google.golang.org/grpc@latest
go get -u google.golang.org/protobuf@latest

實作展示

目錄結構

這是我們本篇會需要撰寫的所有檔案,(深吸一口氣)那我們就準備開始吧!

./
├── api
│   ├── controller               # 控制器
│   │   └── greeter_controller.go
├── bootstrap
│   ├── app.go                   # 核心應用
├── cmd
│   └── main.go                  # 啟動命令
├── domain                       # 領域層
│   └── greeter.go
├── proto                        # Protocol Buffers 相關
│   ├── demo.proto               # .proto 定義文件
│   ├── demo.pb.go               # 生成的 Protobuf 代碼
│   └── demo_grpc.pb.go          # 生成的 gRPC 代碼
├── repository                   
│   └── greeter_repo.go
├── server
│   └── server.go                # gRPC 服務端實作
├── client
│   └── client.go                # gRPC 客戶端實作
├── usecase 
│   └── greeter_usecase.go
├── go.mod
├── go.sum
└── Makefile                     # Makefile 文件

總共:10 個目錄,14 個檔案

定義 Protocol Buffers 文件

  • */proto/ 目錄下撰寫 demo.proto 來去定義 Greeter 服務:
syntax = "proto3";

package chat;

option go_package = "proto/chat";

// Request
message HelloRequest {
  string name = 1;
}

// Response
message HelloReply {
  string message = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
  • syntax = "proto3";:是 Protocol Buffers 的第三版,簡化了定義資料結構的方式。proto3 是目前最常用的版本,提供更現代的語法特性。(如果想了解詳情可以查看底下的額外資料。)
  • package chat;:這行指定了這個 .proto 檔案所屬的 package,在 gRPC 生成的 Go 程式碼(xxx.pb.go、xxx_grpc.pb.go)中,這個 package 名稱會用來組織生成的 類別函式
  • option go_package = "proto/chat";:在生成 Go 程式碼時,gRPC 和 Protobuf 相關的程式碼應該放在哪個 Go package 中。確保這個專案能夠被正確引用。
  • message HelloRequest {}:這個定義了一個資料結構(類似於 Go 中的 struct)用於傳遞資料,名稱為 HelloRequest。在這個資料結構中,有一個屬性:
    • string name = 1;:定義了一個字串類型的屬性 name,用來表示客戶端傳遞過來的名字。這裡的 1 是屬性的字段編號,這個編號在 Protobuf 的序列化和反序列化過程中非常重要,如果要增加更多參數的話,就依序遞增編號就行嘍。
  • service Greeter {}:定義了一個 gRPC 服務 Greeter。在 gRPC 中,服務就是一個遠端呼叫的接口(類似於 API 的 endpoint)。這裡的 Greeter 服務提供了 SayHello 方法。
    • 輸入:接受一個名為 HelloRequest 的資料結構作為輸入。
    • 輸出:回傳一個名為 HelloReply 的資料結構作為回應。

Makefile

Makefile 是一個用來定義專案的編譯規則的檔案,主要應用在自動化編譯和建置流程。它的核心概念來自於 Unix 的 make 工具,可以根據定義的指令(目標)來執行特定的操作,比如編譯程式、生成文件、清理舊的編譯結果等。Makefile 通常用來簡化並自動化重複性工作。

# 定義預設目標
all: server client

# 定義 Protocol Buffer 文件相關變數
PROTO_DIR=proto
PROTO_FILES=$(PROTO_DIR)/*.proto

# 系統架構,amd64 或 arm64
ARCH ?= arm64

# 根據 ARCH 定義不同的 proto-gen 目標
ifeq ($(ARCH), arm64)
proto-gen:
	/opt/homebrew/bin/protoc \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-grpc_out=. \
		--go-grpc_opt=paths=source_relative \
		$(PROTO_FILES)
else
proto-gen:
	/usr/local/bin/protoc \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-grpc_out=. \
		--go-grpc_opt=paths=source_relative \
		$(PROTO_FILES)
endif

#===================================================

# 構建服務器
server:
	go build -o bin/server ./cmd

# 構建客戶端
client:
	go build -o bin/client ./cmd

# 運行服務器,依賴 proto-gen 和 server
run-server: proto-gen server
	./bin/server -mode=server

# 運行客戶端,依賴 proto-gen 和 client
run-client: proto-gen client
	./bin/client -mode=client -name=BeeChat

# 定義虛擬目標
.PHONY: all proto-gen server client run-server run-client clean

# 清理構建產物
clean:
	rm -rf bin/

我把它拆成2段這樣會比較好說明,那第一段的部分就是我們要讓寫好的 .proto 檔案能夠去生成以下兩個gRPC 的程式碼,並確保他們能放在 */proto/ 目錄下:

  • chat.pb.go:由 Protobuf 生成的序列化和反序列化相關的程式碼。
  • chat_grpc.pb.go:由 gRPC 生成的服務端和客戶端相關的程式碼。

第二段的部分如果有看過第22天最後介紹的 Go Native 工具的話,應該猜得到他在幹嘛

  • go build -o bin/server ./cmd 就是用來編譯伺服器端的 Go 程式碼,並生成一個可執行檔放置到 bin/ 目錄,命名為 server。

  • ./bin/server -mode=server 會運行編譯後的伺服器可執行檔,並傳遞 -mode=server 參數。

  • all: server client:當我執行 make all 時,就會自動去執行 serverclient 指令的內容。那之所以放在最上方是因為當我直接執行 make 時,他會自動去抓取第一個寫好的指令來做執行,所以有這樣的好處在。

  • .PHONY: proto-genproto-gen 會被標記為虛擬目標,這意味著每次你執行 make proto-gen 時,Makefile 都會執行定義的指令,而不會去檢查檔案系統中是否已經存在名為 proto-gen 的檔案或目錄。

  • 在專案的 terminal 上輸入下面內容來生成 gRPC 程式碼
# Apple Silicon 指令
make proto-gen
# Intel 指令 (make 後面的順序不影響)
make proto-gen ARCH=amd64

小提示:因為我是在 Mac 上做生成,我電腦上的 cpu 是屬於 arm64 架構,所以前面有先宣告 ARCH ?= arm64 ,但如果是 Windows/Linux 可能就需要使用 amd64 架構。那我要來去做運行時,可以直接使用make proto-gen 但如果是 Windows/Linux 的話則可以使用 make proto-gen ARCH=amd64來去做生成。


實現領域層

  • domain/greeter.go 中定義 Greeter 領域模型:
package domain

type Greeter interface {
	SayHello(name string) (string, error)
}

領域層是應用程式的核心,負責定義業務邏輯和業務規則。在這裡,Greeter 是一個接口(interface),它定義了應用程式的核心功能,即 SayHello 方法。
領域層不依賴於具體的實現細節,使得業務邏輯與技術細節分離。


實現 Repository 層

  • repository/greeter_repo.go 中實現 GreeterRepository:
package repository

import (
	"fmt"
	"grpcDemo/domain"
)

type GreeterRepository struct{}

func NewGreeterRepository() domain.Greeter {
	return &GreeterRepository{}
}

func (repo *GreeterRepository) SayHello(name string) (string, error) {
	return fmt.Sprintf("Hello, %s!", name), nil
}

Repository 層負責具體的數據訪問和操作。在這個例子中,GreeterRepositorydomain.Greeter 接口的具體實現。

  • func SayHello() {}: 實現了 domain.Greeter 接口的方法,返回格式化的問候語。
  • func NewGreeterRepository() {}:提供一個創建 GreeterRepository 實例的方式,並將其作為 domain.Greeter 接口返回。這種做法使得上層不需要知道具體的實現。

實現 Use Case 層

  • usecase/greeter_usecase.go 中實現 GreeterUsecase:
package usecase

import (
	"grpcDemo/domain"
)

type GreeterUsecase struct {
	repo domain.Greeter
}

func NewGreeterUsecase(repo domain.Greeter) *GreeterUsecase {
	return &GreeterUsecase{repo: repo}
}

func (uc *GreeterUsecase) SayHello(name string) (string, error) {
	return uc.repo.SayHello(name)
}

Use Case 層負責應用程式的具體業務用例(use cases),協調領域層和其他層之間的交互。在這裡,GreeterUsecase 使用 domain.Greeter 接口來執行具體的業務邏輯。
Use Case 層作為不同層之間的橋樑,協調它們來完成具體的業務需求。

  • func NewGreeterUsecase() {}:創建並返回一個 GreeterUsecase 的實例,注入所需的 domain.Greeter 實現。

實現 Controller

  • api/controller/greeter_controller.go 中實現 gRPC 服務的控制器:
package controller

import (
	"context"
	pb "grpcDemo/proto"
	"grpcDemo/usecase"
)

type GreeterController struct {
	usecase *usecase.GreeterUsecase
	pb.UnimplementedGreeterServer
}

func NewGreeterController(uc *usecase.GreeterUsecase) *GreeterController {
	return &GreeterController{usecase: uc}
}

func (ctl *GreeterController) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
	message, err := ctl.usecase.SayHello(req.Name)
	if err != nil {
		return nil, err
	}
	return &pb.HelloReply{Message: message}, nil
}

Controller 層負責處理來自外部的請求(如 gRPC 請求),並將這些請求轉發到 Use Case 層進行處理。它是應用程式的接口層,負責協調來自用戶端的輸入和應用邏輯的執行。
Controller 只負責處理請求和回應,不涉及具體的業務邏輯。

  • GreeterController struct {}
    • 欄位 usecase: 持有一個 usecase.GreeterUsecase 的實例,用於調用業務邏輯。
    • 內嵌 pb.UnimplementedGreeterServer: 這是 gRPC 生成的未實現服務器結構,確保未實現的方法會返回錯誤,並允許後續擴展。
  • func NewGreeterController() {}:創建並返回一個 GreeterController 的實例,注入所需的 GreeterUsecase 實現。這樣做促進了依賴注入,使得控制器與具體的 Use Case 實現解耦。
  • func SayHello() {}
    • 簽名: SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error)
    • 作用: 實現 gRPC 定義的 SayHello 方法。接收來自客戶端的 HelloRequest,調用 Use Case 的 SayHello 方法,並將結果封裝成 HelloReply 返回給客戶端。

實現 Server

  • server/server.go 中啟動 gRPC 服務:
package server

import (
	"log"
	"net"

	"google.golang.org/grpc"
	"grpcDemo/api/controller"
	pb "grpcDemo/proto"
	"grpcDemo/repository"
	"grpcDemo/usecase"
)

type Server struct {
	grpcServer *grpc.Server
}

func NewServer() *Server {
	return &Server{
		grpcServer: grpc.NewServer(),
	}
}

func (sv *Server) Start(port string) error {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		return err
	}

	// 初始化 Repository, Usecase, Controller
	repo := repository.NewGreeterRepository()
	greeterUsecase := usecase.NewGreeterUsecase(repo)
	greeterController := controller.NewGreeterController(greeterUsecase)

	// 註冊 Greeter 服務
	pb.RegisterGreeterServer(sv.grpcServer, greeterController)

	log.Printf("gRPC server listening on %s", port)
	return sv.grpcServer.Serve(lis)
}

func (sv *Server) Stop() {
	sv.grpcServer.GracefulStop()
}

Server 層負責啟動和管理 gRPC 服務器,註冊服務器實現,並處理來自客戶端的連接請求。

  • Server struct {}:grpcServer 持有一個 grpc.Server 的實例,負責處理 gRPC 請求。
  • func NewServer() {}:創建並返回一個 Server 的實例,初始化 grpcServer。
  • func Start() {}
    • port string:指定服務器監聽的端口。
    • net.Listen:開始監聽指定的 TCP 端口。
    • pb.RegisterGreeterServer:將 GreeterController 註冊到 gRPC 服務器。
    • Serve:啟動服務器開始處理進來的請求。
  • func Stop() {}:允許正在處理的請求完成後,停止 gRPC 服務器。

實現客戶端

  • client/client.go 中實現 gRPC 客戶端:
    (如果會寫前端的話,也可以直接在前端來做實現的部分,這裡只是做一個簡單的展示)
package client

import (
	"context"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"time"

	"google.golang.org/grpc"
	pb "grpcDemo/proto"
)

func RunClient(address string, name string) {
	conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer func(conn *grpc.ClientConn) {
		err := conn.Close()
		if err != nil {
			log.Fatalf("failed to close connection: %v", err)
		}
	}(conn)

	c := pb.NewGreeterClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	req, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", req.GetMessage())
}

客戶端層負責與 gRPC 服務器進行通信,發送請求並接收回應。在這個例子中,客戶端將發送 SayHello 請求到服務器,並接收問候語回應。


實現應用服務啟動

bootstrap/app.go 用於應用程式的啟動和初始化邏輯

package bootstrap

import (
	"log"

	"grpcDemo/server"
)

func RunServer() {
	srv := server.NewServer()
	if err := srv.Start(":50051"); err != nil {
		log.Fatalf("failed to start server: %v", err)
	}
}

Bootstrap 層負責初始化和啟動應用程式的主要組件

  • func RunServer() {}:負責啟動 gRPC 服務器,這是應用程式的核心部分。
    • server.NewServer():創建一個新的 Server 實例。
    • srv.Start(":50051"):啟動服務器,監聽在端口 50051 上的連接請求。

編寫 main.go

  • cmd/main.go 中啟動服務端或客戶端:
package main

import (
	"flag"
	"log"

	"grpcDemo/bootstrap"
	"grpcDemo/client"
)

func main() {
	mode := flag.String("mode", "server", "啟動模式: server 或 client")
	name := flag.String("name", "World", "客戶端請求的名稱")
	flag.Parse()

	if *mode == "server" {
		bootstrap.RunServer()
	} else if *mode == "client" {
		client.RunClient("localhost:50051", *name)
	} else {
		log.Fatalf("未知的模式: %s", *mode)
	}
}

main.go 是應用程式的入口點,負責解析命令行參數並根據指定的模式啟動服務器或客戶端。


結語

今天我們學會了怎麼開發 gRPC 程式,還順便認識了 Makefile 文件,那明天再來教大家怎麼執行測試寫好的 api 吧!

延伸閱讀


上一篇
【Day24】即時串流通信服務 I | gRPC 簡介
下一篇
【Day26】即時串流通信服務 III | 測試 gRPC 方法 × Apifox/Postman
系列文
Go 快 Go 高效: 從基礎語法到現代Web應用開發27
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言