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 อาจเป็นตัวเลือกที่ดีกว่า
Feature | gRPC | REST |
---|---|---|
Data Format | Binary (Protocol Buffers, a compact and efficient format) | Text (usually JSON or XML) |
Data Transfer Protocol | HTTP/2 | HTTP |
API Definition | Protocol Buffers IDL (Interface Definition Language) | OpenAPI (formerly Swagger) |
API Implementation | Code is generated automatically from the IDL | Must be written manually |
API Discovery | No built-in support for API discovery | API discovery through API documentation |
API Evolution | Versioning handled through service definition | Versioning handled through URL paths or HTTP headers |
API Performance | High performance due to HTTP/2 and binary encoding | Can vary, depending on implementation and network conditions |
API Security | Supports many authentication and security options | Supports 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 ของตัวอย่างนี้