สร้าง GO gRPC project ทำความเข้าใจ gRPC มันดียังไง (โค๊ด gRPC ตัวอย่าง)

Sharing is caring!

gRPC คืออะไร

gRPC (gRPC Remote Procedure Calls) เป็นเฟรมเวิร์ก Remote Procedure Call (RPC) โอเพ่นซอร์สประสิทธิภาพสูงแบบข้ามแพลตฟอร์ม gRPC ถูกสร้างขึ้นครั้งแรกโดย Google ซึ่งใช้โครงสร้างพื้นฐาน RPC สำหรับวัตถุประสงค์ทั่วไปเดียวที่เรียกว่า Stubby เพื่อเชื่อมต่อไมโครเซอร์วิสจำนวนมากที่ทำงานภายในและทั่วทั้งศูนย์ข้อมูลมานานกว่าทศวรรษ

gRPC vs REST

gRPC และ REST เป็นทั้งวิธีในการสร้าง API (Application Programming Interfaces) สำหรับการสื่อสารระหว่างระบบ พวกเขามีความคล้ายคลึงกัน แต่ได้รับการออกแบบโดยคำนึงถึงเป้าหมายที่แตกต่างกันและมีความแตกต่างที่สำคัญหลายประการ ความแตกต่างหลักอย่างหนึ่งระหว่าง gRPC และ REST คือวิธีการเข้ารหัสข้อมูล gRPC ใช้การเข้ารหัสแบบไบนารีที่เรียกว่า Protocol Buffers ในขณะที่ REST ใช้การเข้ารหัสแบบข้อความ เช่น JSON โดยทั่วไปบัฟเฟอร์โปรโตคอลจะเร็วกว่าและมีประสิทธิภาพมากกว่าการเข้ารหัสแบบข้อความ แต่มนุษย์จะอ่านได้น้อยกว่า ข้อแตกต่างอีกประการหนึ่งคือวิธีการสื่อสารระหว่างไคลเอ็นต์และเซิร์ฟเวอร์ gRPC ใช้โมเดลไคลเอ็นต์เซิร์ฟเวอร์ โดยที่ไคลเอนต์ส่งคำขอไปยังเซิร์ฟเวอร์และเซิร์ฟเวอร์ตอบกลับ REST ใช้รูปแบบการตอบสนองคำขอ โดยที่ไคลเอ็นต์ส่งคำขอไปยังเซิร์ฟเวอร์และเซิร์ฟเวอร์จะส่งการตอบสนองกลับ ในแง่ของประสิทธิภาพ โดยทั่วไปแล้ว gRPC จะมีเวลาแฝงต่ำกว่าและปริมาณงานสูงกว่า REST เนื่องจากใช้การเข้ารหัสที่มีประสิทธิภาพมากกว่าและมีประสิทธิภาพมากกว่า รูปแบบการสื่อสาร อย่างไรก็ตาม โดยปกติแล้ว REST จะใช้และเข้าใจได้ง่ายกว่า เนื่องจากใช้แนวคิด HTTP ที่คุ้นเคยและใช้การเข้ารหัสแบบข้อความที่อ่านได้ สุดท้ายแล้ว สิ่งที่คุณเลือกจะขึ้นอยู่กับความต้องการและข้อกำหนดเฉพาะของคุณ หากคุณต้องการ API ประสิทธิภาพสูงที่มีเวลาแฝงต่ำและปริมาณงานสูง gRPC อาจเป็นตัวเลือกที่ดี หากคุณต้องการ API ที่ใช้งานและเข้าใจได้ง่ายกว่า และถ้าความเข้ากันได้กับโครงสร้างพื้นฐานของเว็บที่มีอยู่มีความสำคัญ REST อาจเป็นตัวเลือกที่ดีกว่า

FeaturegRPCREST
Data FormatBinary (Protocol Buffers, a compact and efficient format)Text (usually JSON or XML)
Data Transfer ProtocolHTTP/2HTTP
API DefinitionProtocol Buffers IDL (Interface Definition Language)OpenAPI (formerly Swagger)
API ImplementationCode is generated automatically from the IDLMust be written manually
API DiscoveryNo built-in support for API discoveryAPI discovery through API documentation
API EvolutionVersioning handled through service definitionVersioning handled through URL paths or HTTP headers
API PerformanceHigh performance due to HTTP/2 and binary encodingCan vary, depending on implementation and network conditions
API SecuritySupports many authentication and security optionsSupports many authentication and security options

โดยสรุป gRPC เป็นเฟรมเวิร์ก RPC น้ำหนักเบาที่ทันสมัย ประสิทธิภาพสูง ซึ่งสามารถทำงานผ่าน HTTP/2 ได้ ใช้โปรโตคอลบัฟเฟอร์เป็นภาษานิยามอินเทอร์เฟซและกลไกการทำให้เป็นอนุกรมของข้อความ และใช้ HTTP/2 สำหรับการขนส่ง ในทางกลับกัน REST เป็นรูปแบบสถาปัตยกรรมที่กำหนดชุดข้อจำกัดและคุณสมบัติสำหรับการออกแบบ Web API มันขึ้นอยู่กับ HTTP และโดยทั่วไปจะใช้กับรูปแบบข้อความเช่น JSON หรือ XML

เตรียมเครื่องของคุณให้พร้อม เริ่มกันเลย

ถ้าคุณเองเป็น GO Developer คุณเพียงเพิ่ม Protocol Buffer Compiler นี้ก็พอ หรือคุณสามารถทำตามลิ้งนี้ GO gRPC Quick Starter

$ go mod init github.com/pool13433/grpc-go

$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/protobuf

โครงสร้าง Project

- grpc-go
  |- greet
    |- client
      |-greet.go
      |-main.go
    |- proto
      |-greet.proto
    |- server
      |-greet.go
      |-main.go
      |-server.go
   |-go.mod
   |-Makefile

– grpc-go/greet/client/greet.go

package main

import (
	"context"
	pb "github.com/pool13433/grpc-go/greet/proto"
	"log"
)

func doGreet(c pb.GreetServiceClient) {
	log.Println("doGreet was invoked")
	r, err := c.Greet(context.Background(), &pb.GreetRequest{FirstName: "Clement"})

	if err != nil {
		log.Fatalf("Could not greet: %v\n", err)
	}

	log.Printf("Greeting: %s\n", r.Result)
}

– grpc-go/greet/client/main.go

package main

import (
	pb "github.com/pool13433/grpc-go/greet/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
)

var addr string = "localhost:50051"

func main() {
	opts := []grpc.DialOption{}

	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))

	conn, err := grpc.Dial(addr, opts...)

	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}

	defer conn.Close()
	c := pb.NewGreetServiceClient(conn)

	doGreet(c)
}

– grpc-go/greet/proto/greet.proto

syntax = "proto3";

package greet;

option go_package = "github.com/pool13433/grpc-go/greet/proto";

message GreetRequest {
  string first_name = 1;
}

message GreetResponse {
  string result = 1;
}

service GreetService {
  rpc Greet(GreetRequest) returns (GreetResponse);
};

– grpc-go/greet/server/greet.go

package main

import (
	"context"
	pb "github.com/pool13433/grpc-go/greet/proto"
	"log"
)

func (*Server) Greet(ctx context.Context, in *pb.GreetRequest) (*pb.GreetResponse, error) {
	log.Printf("Greet was invoked with %v\n", in)
	return &pb.GreetResponse{Result: "Hello " + in.FirstName}, nil
}

– grpc-go/greet/server/main.go

package main

import (
	pb "github.com/pool13433/grpc-go/greet/proto"
	"google.golang.org/grpc"
	"log"
	"net"
)

var addr = "0.0.0.0:50051"

func main() {
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatalf("Failed to listen: %v\n", err)
	}
	defer lis.Close()
	log.Printf("Listening at %s\n", addr)

	opts := []grpc.ServerOption{}
	s := grpc.NewServer(opts...)
	pb.RegisterGreetServiceServer(s, &Server{})
	defer s.Stop()
	if err := s.Serve(lis); err != nil {
		log.Fatalf("Failed to serve: %v\n", err)
	}
}

– grpc-go/greet/server/server.go

package main

import pb "github.com/pool13433/grpc-go/greet/proto"

type Server struct {
	pb.GreetServiceServer
}

– grpc-go/go.mod

module github.com/pool13433/grpc-go

go 1.19

require (
	google.golang.org/grpc v1.51.0
	google.golang.org/protobuf v1.28.1
)

require (
	github.com/golang/protobuf v1.5.2 // indirect
	golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
	golang.org/x/text v0.4.0 // indirect
	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
)

– grpc-go/Makefile (สร้าง command script เฉพาะเจาะจง คำสั่งยาว ๆ ให้เป็น script สั้น ๆ ใช้งานง่าย ๆ )

BIN_DIR = bin
PROTO_DIR = proto
SERVER_DIR = server
CLIENT_DIR = client

ifeq ($(OS), Windows_NT)
	SHELL := powershell.exe
	.SHELLFLAGS := -NoProfile -Command
	SHELL_VERSION = $(shell (Get-Host | Select-Object Version | Format-Table -HideTableHeaders | Out-String).Trim())
	OS = $(shell "{0} {1}" -f "windows", (Get-ComputerInfo -Property OsVersion, OsArchitecture | Format-Table -HideTableHeaders | Out-String).Trim())
	PACKAGE = $(shell (Get-Content go.mod -head 1).Split(" ")[1])
	CHECK_DIR_CMD = if (!(Test-Path $@)) { $$e = [char]27; Write-Error "$$e[31mDirectory $@ doesn't exist$${e}[0m" }
	HELP_CMD = Select-String "^[a-zA-Z_-]+:.*?\#\# .*$$" "./Makefile" | Foreach-Object { $$_data = $$_.matches -split ":.*?\#\# "; $$obj = New-Object PSCustomObject; Add-Member -InputObject $$obj -NotePropertyName ('Command') -NotePropertyValue $$_data[0]; Add-Member -InputObject $$obj -NotePropertyName ('Description') -NotePropertyValue $$_data[1]; $$obj } | Format-Table -HideTableHeaders @{Expression={ $$e = [char]27; "$$e[36m$$($$_.Command)$${e}[0m" }}, Description
	RM_F_CMD = Remove-Item -erroraction silentlycontinue -Force
	RM_RF_CMD = ${RM_F_CMD} -Recurse
	SERVER_BIN = ${SERVER_DIR}.exe
	CLIENT_BIN = ${CLIENT_DIR}.exe
else
	SHELL := bash
	SHELL_VERSION = $(shell echo $$BASH_VERSION)
	UNAME := $(shell uname -s)
	VERSION_AND_ARCH = $(shell uname -rm)
	ifeq ($(UNAME),Darwin)
		OS = macos ${VERSION_AND_ARCH}
	else ifeq ($(UNAME),Linux)
		OS = linux ${VERSION_AND_ARCH}
	else
		$(error OS not supported by this Makefile)
	endif
	PACKAGE = $(shell head -1 go.mod | awk '{print $$2}')
	CHECK_DIR_CMD = test -d $@ || (echo "\033[31mDirectory $@ doesn't exist\033[0m" && false)
	HELP_CMD = grep -E '^[a-zA-Z_-]+:.*?\#\# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?\#\# "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
	RM_F_CMD = rm -f
	RM_RF_CMD = ${RM_F_CMD} -r
	SERVER_BIN = ${SERVER_DIR}
	CLIENT_BIN = ${CLIENT_DIR}
endif

.DEFAULT_GOAL := help
.PHONY: greet blog calculator help
project := greet calculator blog

all: $(project) ## Generate Pbs and build

greet: $@ ## Generate Pbs and build for greet
calculator: $@ ## Generate Pbs and build for calculator
blog: $@ ## Generate Pbs and build for blog

$(project):
	@${CHECK_DIR_CMD}
	protoc -I$@/${PROTO_DIR} --go_opt=module=${PACKAGE} --go_out=. --go-grpc_opt=module=${PACKAGE} --go-grpc_out=. $@/${PROTO_DIR}/*.proto
	go build -o ${BIN_DIR}/$@/${SERVER_BIN} ./$@/${SERVER_DIR}
	go build -o ${BIN_DIR}/$@/${CLIENT_BIN} ./$@/${CLIENT_DIR}

test: all ## Launch tests
	go test ./...

clean: clean_greet clean_calculator clean_blog ## Clean generated files
	${RM_F_CMD} ssl/*.crt
	${RM_F_CMD} ssl/*.csr
	${RM_F_CMD} ssl/*.key
	${RM_F_CMD} ssl/*.pem
	${RM_RF_CMD} ${BIN_DIR}

clean_greet: ## Clean generated files for greet
	${RM_F_CMD} greet/${PROTO_DIR}/*.pb.go

clean_calculator: ## Clean generated files for calculator
	${RM_F_CMD} calculator/${PROTO_DIR}/*.pb.go

clean_blog: ## Clean generated files for blog
	${RM_F_CMD} blog/${PROTO_DIR}/*.pb.go

rebuild: clean all ## Rebuild the whole project

bump: all ## Update packages version
	go get -u ./...

about: ## Display info related to the build
	@echo "OS: ${OS}"
	@echo "Shell: ${SHELL} ${SHELL_VERSION}"
	@echo "Protoc version: $(shell protoc --version)"
	@echo "Go version: $(shell go version)"
	@echo "Go package: ${PACKAGE}"
	@echo "Openssl version: $(shell openssl version)"

help: ## Show this help
	@${HELP_CMD}

เมื่อโค๊ดพร้อมแล้ว มาทดสอบกัน

$ make greet

จะมีไฟล์ที่ proto compile (protoc) ใหม่เพิ่มเข้ามา

— grpc-go/greet/proto/greet.pb.go
— grpc-go/greet/proto/greet_grpc.pb.go
— grpc-go/bin/greet/server
— grpc-go/bin/greet/client

$ ./bin/greet/server
2023/01/07 21:42:00 Listening at 0.0.0.0:50051
2023/01/07 21:42:16 Greet was invoked with first_name:"Clement" <<-- เมื่อมี client call เข้ามาจะมี log message

รัน gRPC server

$ ./bin/greet/client
2023/01/07 21:42:16 doGreet was invoked
2023/01/07 21:42:16 Greeting: Hello Clement

รัน gRPC client

สรุปท้ายบทความ

ฝากเป็นลิ้ง github code ของตัวอย่างนี้

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *