มารู้จักกับ gRPC และ Go กัน
/ 15 min read
Last Updated:
สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
รู้จักกับ gRPC
gRPC (gRPC Remote Procedure Calls) 1 เป็น framework การสื่อสารที่พัฒนาโดย Google ใช้สำหรับการสร้างระบบที่สามารถสื่อสารกันระหว่าง application ในเครือข่ายได้อย่างรวดเร็วและมีประสิทธิภาพมากขึ้น โดย gRPC ใช้ protocal HTTP/2 สำหรับการส่งข้อมูล และใช้ Protocol Buffers (protobuf) เป็นรูปแบบในการ serialize ข้อมูลเพื่อให้การรับส่งข้อมูลเกิดขึ้นอย่างรวดเร็วมากขึ้น

ภาพจาก https://trends.stackoverflow.co 2
อย่างที่เราทราบกัน ในปัจจุบันถ้าพูดถึง API protocal การส่งข้อมูลที่เป็นที่นิยมมากที่สุด ก็คงปฎิเสธไม่ได้ว่ามันคือ Rest API เนื่องจากเป็นมาตรฐานที่เป็นที่นิยมและ implement ได้ง่ายบน application รูปแบบต่างๆ ทีนี้ หากเรา ลองเทียบ ความแตกต่างระหว่าง gRPC กับ REST API ว่ามีความแตกต่างอะไรบ้าง เราจะเจอว่ามันมีความแตกต่างกันดังนี้
- Protocal การส่งข้อมูล
- REST API: ใช้ HTTP/1.1 ในการส่งข้อมูล โดยมีการส่ง request และ response แบบ text-based (JSON หรือ XML)
- gRPC: ใช้ HTTP/2 ซึ่งมีการส่งข้อมูลแบบ binary ที่บีบอัดด้วย Protocol Buffers (protobuf) ทำให้มีประสิทธิภาพในการรับส่งสูงขึ้น
- รูปแบบของข้อมูล
- REST API: ใช้ JSON หรือ XML ซึ่งเป็น text-based format ที่เข้าใจได้ง่าย แต่มีขนาดใหญ่และต้องใช้เวลาในการ parse มากกว่า
- gRPC: ใช้ Protocol Buffers ซึ่งเป็น binary format ที่มีขนาดเล็กกว่าและประมวลผลได้เร็วกว่า JSON
- การสนับสนุนการเรียกแบบ asynchronous
- REST API: การทำงานแบบ synchronous เป็นพื้นฐาน การทำงานแบบ asynchronous สามารถทำได้ แต่ต้องจัดการผ่าน Backend Application ในแต่ละภาษาเพิ่มเติม
- gRPC: สนับสนุนการทำงานแบบ asynchronous และมี built-in support สำหรับการทำงานแบบ bidirectional streaming (สามารถส่งและรับข้อมูลระหว่าง client และ server ได้ในเวลาเดียวกัน)
- การใช้งานแบบหลายภาษา
- REST API: รองรับทุกภาษาโปรแกรมเนื่องจากทำงานบน HTTP และ JSON ซึ่งทุกภาษาสามารถทำงานได้
- gRPC: มีเครื่องมือที่สามารถสร้าง client และ server code ในหลายภาษา เช่น Go, Java, Python, C++ เป็นต้น ด้วยการใช้ protobuf แต่ทั้งนี้ก็จะขึ้นอยู่กับภาษาที่ support ด้วยเช่นกัน
- Configuration
- REST API: ตั้งค่าง่ายกว่า และมีการใช้งานทั่วไปมากกว่า เหมาะกับกรณีที่ไม่ต้องการการเชื่อมต่อแบบ realtime
- gRPC: มีความซับซ้อนในการตั้งค่าเริ่มต้นมากกว่า โดยเฉพาะการจัดการ protobuf และ HTTP/2 แต่เหมาะสมกับ application ที่ต้องการประสิทธิภาพสูงและการเชื่อมต่อแบบ real-time
ข้อดีของ gRPC:
- มีประสิทธิภาพสูง เนื่องจากใช้ binary format ในการสื่อสาร
- สนับสนุนการทำงานแบบ bidirectional streaming ซึ่ง REST API ไม่สามารถทำได้
- สามารถใช้ในระบบที่ต้องการ latency ต่ำ
ข้อดีของ REST API:
- เข้าใจง่ายและตั้งค่าใช้งานง่ายกว่า
- เข้ากันได้กับ HTTP ทั่วไปและมีการใช้งานแพร่หลาย
เพราะฉะนั้น gRPC ไม่ได้ถูกสร้างมาเพื่อแทน Rest API แต่มันถูกสร้างมาเพื่อ “รีด” ประสิทธิภาพการส่งข้อมูลสูงสุดออกมา โดยปกติ use case ที่มักจะใช้ gRPC จะมีตั้งแต่
- Microservices Communication ใช้สำหรับการสื่อสารระหว่าง microservices เนื่องจากประสิทธิภาพสูงและ latency ต่ำ
- Real-Time Data Streaming เหมาะสำหรับระบบที่ต้องการการส่งข้อมูลแบบ real-time เช่น video streaming หรือ IoT data
- Low-Latency Systems ใช้ในระบบที่ต้องการ latency ต่ำ เช่น trading systems หรือเกมออนไลน์
- Distributed Systems ใช้ในระบบ distributed เพื่อให้การสื่อสารระหว่าง node เป็นไปอย่างมีประสิทธิภาพ
ทีนี้ เรามาลองทำความเข้าใจหลักการของ gRPC เพิ่มเติมกัน
หลักการของ gRPC
ใน gRPC application ฝั่ง Client สามารถเรียกใช้งาน Method บน application ฝั่ง Server ที่อยู่บนเครื่องอื่นได้โดยตรง เสมือนว่าเป็นการเรียกใช้งาน object ภายใน project ตัวเอง ซึ่งทำให้การสร้าง application และ service แบบ Distributed เป็นเรื่องง่ายขึ้น 3
เช่นเดียวกับระบบ RPC อื่น ๆ gRPC มีแนวคิดหลักอยู่ที่การนิยาม Service โดยกำหนด Method ที่สามารถเรียกใช้งานได้ พร้อมกับกำหนด parameter และ return type เอาไว้ได้
- ส่วนในฝั่ง Server จะต้องทำการ Implement interface นี้ และรัน gRPC Server เพื่อจัดการ request จากฝั่ง Client
- ส่วนในฝั่ง Client จะมี Stub ซึ่งมี Method เดียวกันกับฝั่ง Server
เพื่อให้เข้าใจหลักการของ gRPC จะขออธิบายผ่านองค์ประกอบใน Diagram นี้ทั้งหมด 3 องค์ประกอบใหญ่ๆคือ
- gRPC Server
gRPC Server เป็นส่วนที่เปิดให้บริการ (service) โดยในภาพตัวอย่างนี้ gRPC Server เชื่อมต่อกับ C++ Service ซึ่งหมายถึง service ที่พัฒนาด้วยภาษา C++ เมื่อ client ต้องการสื่อสารหรือขอข้อมูลจาก server จะทำการส่ง request ผ่าน protocal gRPC ไปยัง server นี้
ตัว gRPC Server ทำหน้าที่รับคำขอ (request) จาก client และทำการส่งคำตอบ (response) กลับไป โดยที่ข้อมูลที่รับส่งนั้นจะอยู่ในรูปแบบ protobuf (Protocol Buffers) หรือ binary format
- gRPC Stub
ในฝั่ง client (ในภาพนี้มี 2 ตัวอย่างคือ Ruby Client และ Android-Java Client) จะมีส่วนที่เรียกว่า gRPC Stub ซึ่งเป็นตัวแทนของ function ที่อยู่บน server ทำให้การเรียกใช้ function บน server สามารถทำได้เหมือนกับการเรียกใช้ฟังก์ชัน local บน client (ทั้ง 2 ฝั่งจะรู้จักกันและรู้ว่ามี function อะไรอยู่บ้าง ลักษณะคล้ายๆกับการ import library เข้าไป)
โดย ฝั่ง client ไม่จำเป็นต้องรู้รายละเอียดเชิงลึกของ server เพียงแค่ใช้ stub นี้เพื่อสื่อสารกับ server เท่านั้น
- Proto Request และ Proto Response
gRPC ใช้ Protocol Buffers ในการ serialize ข้อมูลที่ส่งระหว่าง client และ server ข้อมูลที่ส่งจาก client ไปยัง server จะถูกเรียกว่า Proto Request และคำตอบที่ server ส่งกลับไปให้ client จะเรียกว่า Proto Response ข้อมูลเหล่านี้จะถูกแปลงเป็น binary format เพื่อให้สามารถรับส่งได้อย่างรวดเร็วและประหยัด bandwidth
เพื่อให้ get ภาพทั้งหมดของ gRPC ผมขออธิบาย RPC เพิ่มเติมเพื่อให้เกิดความเข้าใจที่มากขึ้น
RPC (Remote Procedure Call) คือรูปแบบการสื่อสารระหว่าง application ที่ทำให้เราสามารถเรียกใช้ function หรือ method ในระบบหนึ่งจากอีกระบบหนึ่งได้เสมือนเป็นการเรียกใช้ function ภายในระบบเดียวกัน แม้ว่า application ทั้งสองจะทำงานบนเครื่องหรือเซิร์ฟเวอร์ที่แตกต่างกันก็ตาม
ถ้าใครที่คุ้นชื่อกับ SOAP (Simple Object Access Protocol) ที่ต้องกำหนดมาตรฐานในการส่ง XML นั่นแหละครับ คือ หนึ่งในรูปแบบของ RPC ที่ใช้ในการสื่อสารระหว่างระบบ client และ server

Ref: https://medium.com/@rathnaweeraatheesh72/basic-rpc-implemented-system-in-java-8a0f359129a0
ปัญหาที่ถือว่าเป็นปัญหาจุกจิกของ RPC ดังเดิม (ก่อนยุค gRPC) ก็จะมีตั้งแต่
- ใช้รูปแบบข้อมูล text-based (เช่น XML หรือ JSON) ทำให้ข้อมูลมีขนาดใหญ่ทำให้ส่งข้อมูลได้ช้า
- RPC แบบดั้งเดิมแม้จะรองรับหลายภาษา แต่การใช้ text-based format เช่น XML หรือ JSON ยังคงต้องพึ่งพาการแปลงข้อมูลที่ซับซ้อนในแต่ละภาษา ทำให้เกิดความผิดพลาดได้ง่าย
- RPC แบบดั้งเดิมมักไม่รองรับการทำงานแบบ bidirectional streaming หรือการส่งข้อมูลแบบต่อเนื่อง ทำให้การส่งข้อมูลขนาดใหญ่หรือการทำงานแบบ real-time ยากขึ้น ตัวอย่างเช่น การส่งข้อมูลวิดีโอหรือการอัพเดทข้อมูล realtime
- RPC แบบดั้งเดิมมักไม่มีมาตรฐานการเข้ารหัสข้อมูลในตัว เช่นใน XML-RPC หรือ JSON-RPC ซึ่งการจะเพิ่มความปลอดภัยในการสื่อสารต้องทำผ่านการตั้งค่าพิเศษและเครื่องมืออื่นๆเข้ามาช่วย
โดย gRPC เป็นหนึ่งใน framework ที่ทำงานบนแนวคิดของ RPC โดยมีการขยายความสามารถและเพิ่มประสิทธิภาพขึ้นจากการใช้ protocal และเครื่องมือที่ทันสมัย gRPC ยืนพื้นบน HTTP/2 ซึ่งมีคุณสมบัติที่ดีกว่า HTTP/1.x ที่ใช้ในระบบ RPC แบบด��้งเดิม (เช่น XML-RPC หรือ JSON-RPC) ทำให้สามารถรับส่งข้อมูลได้แบบ multiplexing (สามารถส่งหลายคำขอพร้อมกันได้โดยไม่ต้องเปิดหลายการเชื่อมต่อ) และลด latency ในการส่งข้อมูล
https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTnHltqY1iaRrklQs6TdJkI5lViOlSn5szAWg &s
Ref: https://coolicehost.com/http2-protocol.html
นอกจากนี้ gRPC ยังใช้ Protocol Buffers (protobuf) ซึ่งเป็น binary format ทำให้การรับส่งข้อมูลมีขนาดเล็กลงและมีความเร็วในการประมวลผลสูงอีกด้วย
รวมถึง gRPC มีการรองรับ bidirectional streaming ซึ่ง client และ server สามารถส่งข้อมูลหากันได้พร้อมกันและต่อเนื่อง และรองรับการเข้ารหัส TLS (Transport Layer Security) เป็นค่าเริ่มต้นเอาไว้เลย ทำให้การรับส่งข้อมูลระหว่าง client และ server มีความปลอดภัยสูงขึ้นด้วยเช่นกัน
นี่คือหลักการโดยประมาณของ gRPC ทีนี้ เราจะลองมาขยายความคำว่า “Protocol Buffers” (protobuf) กันอีกสักนิดนึงว่ามันคืออะไร และเราสามารถนำ protobuf ไป implement แต่ละส่วนยังไงได้บ้าง
Protocol Buffers
Protocol Buffers (Protobuf) คือรูปแบบการจัดเก็บและถ่ายโอนข้อมูลแบบ binary ที่พัฒนาโดย Google ซึ่งทำงานโดยการกำหนดโครงสร้างข้อมูลในไฟล์ .proto
จากนั้นจะทำการแปลงข้อมูลเป็น binary ที่ขนาดเล็กกว่ารูปแบบ JSON หรือ XML มาก ทำให้ประสิทธิภาพในการส่งข้อมูลระหว่างระบบเพิ่มขึ้น และใช้เวลาในการประมวลผลน้อยลง
โดยหลักการทำงานของ Protobuf คือ
- กำหนดโครงสร้างข้อมูลในไฟล์
.proto
- compile ไฟล์
.proto
ด้วยเครื่องมือprotoc
เพื่อสร้าง code ที่สามารถ serialize และ deserialize ข้อมูลได้ - ใช้งาน code ที่สร้างขึ้นสำหรับการรับและส่งข้อมูลใน application ออกมาได้
โดยไฟล์ .proto
ใช้เพื่อกำหนดรูปแบบและโครงสร้างของข้อมูลที่จะใช้ใน application โดยมีการกำหนดชนิดข้อมูล (data types) และ field ที่ใช้ในข้อมูล
นี่คือตัวอย่างของไฟล์ .proto
syntax = "proto3";
message Person { string name = 1; // ชื่อของคนนี้ int32 id = 2; // ID ของคนนี้ string email = 3; // อีเมลของคนนี้}
หลังจากเขียนไฟล์ .proto
แล้ว เราสามารถใช้ protoc
เพื่อ compile code ในหลายภาษา เช่น Go, Python, Java, และอื่น ๆ เพื่อใช้งานข้อมูลนี้ใน application
หลังจาก compile code เรียบร้อยแล้ว เราจะไฟล์ที่ support เป็นเหมือน library ในภาษานั้นๆ เราก็จะสามารถ import ใช้งานได้ เช่น ในภาษา Python จะมีลักษณะดังนี้
from person_pb2 import Person
# สร้าง object ของ Person
# แปลงเป็น binary เพื่อส่งข้อมูลbinary_data = person.SerializeToString()
# การรับข้อมูลและแปลงกลับเป็น objectnew_person = Person()new_person.ParseFromString(binary_data)
print(new_person)
รวมถึง ในการใช้ Protocol Buffers (Protobuf) ร่วมกับ RPC (Remote Procedure Call) เราสามารถกำหนด method parameters และ return types ในไฟล์ .proto
เพื่อใช้ในการสื่อสารระหว่าง client และ server ผ่าน gRPC ได้โดยตรง (ตามที่อธิบายไปตอนต้น) วิธีการคือการกำหนด service ที่มี method สำหรับการเรียกใช้ function ต่าง ๆ พร้อมกับกำหนดชนิดของข้อมูลที่ใช้สำหรับ request และ response ใน .proto
ตัวอย่าง .proto
syntax = "proto3";
package example;
service UserService { // Method ที่ใช้ส่ง PersonRequest และรับคืนเป็น PersonResponse rpc GetUser (PersonRequest) returns (PersonResponse);}
message PersonRequest { int32 id = 1; // ใช้ส่งค่า ID ของผู้ใช้}
message PersonResponse { string name = 1; // ชื่อของผู้ใช้ int32 id = 2; // ID ของผู้ใช้ string email = 3; // อีเมลของผู้ใช้}
ในตัวอย่างนี้ เราได้กำหนด service ชื่อ UserService
ที่มี method ชื่อ GetUser
ซึ่งจะรับค่า request ที่เป็น PersonRequest
และส่งค่า response ที่เป็น PersonResponse
- PersonRequest: ข้อมูลที่ client จะส่งไปยัง server ซึ่งในกรณีนี้เป็น
id
ของผู้ใช้ - PersonResponse: ข้อมูลที่ server จะส่งกลับไปให้ client ซึ่งมี
name
,id
, และemail
ทีนี้ ที่เหลือก็จะขึ้นอยู่กับ server และ client ว่า
- Server จะ implement code ตาม protocol ที่กำหนดผ่าน gRPC ได้อย่างไร
- Client จะ implement การเรียกใช้ request, response ที่กำหนดผ่าน gRPC ได้อย่างไร
ก็เหมือนกับ diagram ที่เราะอธิบายไปตอนแรกได้ ทีนี้ เพื่อให้ทุกคนเห็นภาพตอน implement เราจะขอหยิบภาษา Go ซึ่งเป็นอีกหนึ่งภาษายอดนิยมที่คนมักใช้ implement gRPC กัน
Step to Implement gRPC
ก่อนจะเริ่ม implement เราลองมาเรียบเรียง step ที่เราต้องทำก่อนว่าเราต้องทำอะไรบ้าง โดยสิ่งที่เราจะต้องทำคือ
- กำหนด Protocol Buffers (
.proto
file) สร้างไฟล์.proto
เพื่อกำหนดโครงสร้างข้อมูล (message) และบริการ (service) ที่ต้องการใช้งานใน gRPC
syntax = "proto3";
service UserService { rpc GetUser (PersonRequest) returns (PersonResponse);}
message PersonRequest { int32 id = 1;}
message PersonResponse { string name = 1; string email = 2;}
- compile file
.proto
โดยใช้protoc
(Protocol Buffers compiler) เพื่อแปลงไฟล์.proto
เป็น code stub สำหรับการใช้งานในภาษาโปรแกรมที่เราจะใช้งาน เช่น Go, Python, Java เป็นต้น (ซึ่งในทีนี้เราจะใช้ภาษา Go กัน)
# ตัวอย่างคำสั่งที่ใช้ compile protocprotoc --go_out=. --go-grpc_out=. path/to/your.proto
- Implement gRPC Server โดยเขียน code ที่ทำหน้าที่เป็น gRPC Server โดย implement service ที่กำหนดในไฟล์
.proto
และสร้างเป็น server gRPC Server ขึ้นมา
type server struct { pb.UnimplementedUserServiceServer}
// implement GetUser ตามที่ตกลงไว้ใน .protofunc (s *server) GetUser(ctx context.Context, req *pb.PersonRequest) (*pb.PersonResponse, error) { return &pb.PersonResponse{ Name: "John Doe", }, nil}
// และสร้างเป็น Server gRPC ขึ้นมาfunc main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) }
grpcServer := grpc.NewServer() pb.RegisterUserServiceServer(grpcServer, &server{})
if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}
- Implement gRPC Client เขียน code ฝั่ง client เพื่อเรียกใช้งาน method ที่ server เปิดให้ใช้บริการได้
func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel()
creds := insecure.NewCredentials()
// Create a new gRPC client connection (ต่อไปยังที่เดียวกับ gRPC server) conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("Failed to create client: %v", err) } defer conn.Close()
client := pb.NewUserServiceClient(conn) req := &pb.PersonRequest{Id: 123}
// เรียกใช้คำสั่งเดียวกับที่กำหนดไว้ใน .proto res, err := client.GetUser(context.Background(), req) if err != nil { log.Fatalf("could not get user: %v", err) }
log.Printf("User: %s, Email: %s", res.GetName(), res.GetEmail())}
และนี่คือ 4 Step โดยประมาณสำหรับการ implement gRPC ที่เราจะ follow กัน
Init project และ กำหนด Protocal Buffer
ดังนั้นเราจะเริ่มจาก Step 0 (ก่อน Step แรก) กันก่อนนั่นคือ init project Go กัน โดยทำการสร้าง folder สำหรับ project go ขึ้นมาและทำการ init project ตามท่ามาตรฐานของ Go
** สำหรับหัวข้อนี้ เราจะไม่ลงพื้นฐาน Go หากต้องการซึมซับพื้นฐาน Go ก่อนสามารถติดตามผ่าน GoAPI Essential Series ได้ผ่านที่นี่ก่อนได้
https://docs.mikelopster.dev/c/goapi-essential/intro
# init project ด้วยgo mod init <ชื่อ project>
# examplego mod init grpc-hello-world
และทำการลง library ที่เกี่ยวข้องกับ gRPC ของ go
go get google.golang.org/grpcgo install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
grpc
เป็น gRPC library สำหรับ Go ที่ใช้ในการสร้าง client และ server ที่สื่อสารผ่าน protocal gRPCprotoc-gen-go
เป็น Plugin สำหรับ Protocol Buffers (Protobuf) ที่ทำงานร่วมกับคำสั่งprotoc
ซึ่งใช้ในการแปลงไฟล์.proto
ไปเป็น code Go ที่สามารถใช้งานใน Go ได้protoc-gen-go-grpc
เป็น Plugin ที่ทำงานร่วมกับprotoc
เหมือนกับprotoc-gen-go
แต่ใช้สำหรับสร้างโค้ดที่เกี่ยวข้องกับ gRPC โดยเฉพาะ โดยเป็นตัวช่วยสร้าง stub ที่จำเป็นสำหรับ client และ server ของ gRPC โดยอัตโนมัติจากไฟล์.proto
เท่านี้ก็เป็นการ init project go และพร้อมสำหรับลุย gRPC แล้ว
สร้าง Protobuf และ compile
Step ต่อมา (Step แรกของการ implement ตามที่เราเคยพูดกัน) เราจะเริ่มสร้าง file protobuf สำหรับกำหนด spec ของ protobuf กัน โดยปกติ เพื่อให้การจัดการ structure กันได้ง่าย เรามักจะวางกันใน folder proto
กัน เพื่อให้รู้ว่าเป็นสถานที่สำหรับเก็บ protobuf เอาไว้
เราจะลองสร้าง service อย่างง่ายกัน โดยสร้าง service sayHello
ขึ้นมา โดยโจทย์มีเพียงแค่รับ name ผ่าน request เข้ามาและแสดงข้อความ response name นั้นผ่าน console และคืนเป็น message ที่บอกว่า Hello <name>
กลับไป ดังนั้นสิ่งที่เราต้องกำหนดคือ
- มี rpc service หนึ่งตัวชื่อ
sayHello
sayHello
มีการรับ request 1 field คือname
- และ
sayHello
มีการ return response 1 field คือmessage
กลับไป
เราทำการสร้างไฟล์ proto/helloworld.proto
เป็นหน้าตาประมาณนี้ออกมา
// กำหนด version ของ Protocol Buffers ที่ใช้syntax = "proto3";
// กำหนด package สำหรับ Go (ใช้สำหรับ reference ภายใน go)option go_package = "grpc-hello-world/proto";
// นิยาม service ชื่อ Greeterservice Greeter { // กำหนด RPC method ชื่อ SayHello // รับ HelloRequest และส่งคืน HelloReply rpc SayHello (HelloRequest) returns (HelloReply);}
// นิยาม message สำหรับคำขอmessage HelloRequest { // field name เป็น string มี tag number 1 string name = 1;}
// นิยาม message สำหรับการตอบกลับmessage HelloReply { // field message เป็น string มี tag number 1 string message = 1;}
หลายคนเห็น code นี้ก็อาจจะสงสัยหนึ่งอย่าง ไอเลข 1 นี่หมายความว่าอย่างไร ?
tag number (เลข 1 ที่เราเห็น) คือ เลข tag ใน Protocol Buffers เป็นหมายเลขที่กำหนดให้กับแต่ละ field ใน message
ที่ใช้เพื่อระบุและแยกแยะ field ต่าง ๆ ในการส่งข้อมูล เมื่อข้อมูลถูกส่งผ่านเครือข่ายโดยใช้ protobuf ข้อมูลจะถูกแปลงเป็น binary format และ tag number จะถูกใช้เป็นตัวอ้างอิงสำหรับแต่ละ field ในข้อมูลแทนที่จะใช้ชื่อของ field เอง
เพราะฉะนั้น ถ้าเกิดมีการรับ / ส่งหลาย field ก็จะต้องใช้ tag number ที่แตกต่างกัน เช่นแบบนี้
message Person { string name = 1; int32 id = 2; string email = 3;}
โดย ค่าของ Tag number นั้น ****สามารถมีค่าตั้งแต่ 1
ถึง 2^29 - 1
แต่โดยทั่วไปควรใช้เลขระหว่าง 1
ถึง 15
สำหรับ field ที่ใช้งานบ่อย เพราะ tag number ช่วงนี้ใช้พื้นที่น้อยกว่าในการ serialize ข้อมูล
โดย ข้อควรระวัง Tag number นั้น จะมีดังต่อไปนี้
- Tag number 1-15 ใช้พื้นที่น้อยกว่า tag number ที่มีค่าสูงกว่า ดังนั้นควรกำหนด tag number ที่ใช้บ่อย ๆ ให้อยู่ในช่วงนี้
- Tag number 16-2047 จะใช้พื้นที่เพิ่มขึ้นเล็กน้อยในการ serialize
- Tag number ต้องไม่ซ้ำกันในแต่ละ
message
เพื่อหลีกเลี่ยงความสับสนในการอ้างอิง field ต่างๆ
ok เมื่อกำหนด protobuf แล้วเรียบร้อย step ต่อมา (ตาม step ที่ 2) เราจะทำการ compile code protobuf เพื่อแปลงไฟล์ .proto
เป็น code stub สำหรับการใช้งานในภาษา Go ออกมา
โดยคำสั่งที่เราจะใช้คือ
# สำหรับชาว Linux / Mac ที่อาจจะใช้ไม่ได้ เนื่องจาก protoc ยังไม่ได้ระบุ GOPATH ที่อ้างอิงให้มาใช้งานคำสั่ง protoc ได้# สามารถใช้คำสั่งนี้ได้export PATH="$PATH:$(go env GOPATH)/bin"
# คำสั่งสำหรับ compile protobufprotoc --go_out=. --go-grpc_out=. proto/helloworld.proto
เราก็จะได้ ผลลัพธ์ออกมาเป็น go file ที่ทำการ implement ตาม .proto
เป็นที่เรียบร้อย

ตอนนี้ protobuf เราก็พร้อมสำหรับการนำไป implement ทั้งฝั่ง gRPC Server และ gRPC client และ step ต่อมาเราจะเริ่ม implement gRPC Server กัน เพื่อใส่ logic ให้กับ sayHello
ตามที่กำหนดไว้ใน protobuf กัน
gRPC Server
Step ถัดมา เราจะเริ่ม implement function sayHello
ในส่วนของ logic กัน โดยสิ่งที่ gRPC Server ต้องทำคือ
- ทำการ import library (ที่ผ่าน compile protobuf มา) นำเข้ามา implement ที่ go
- implement function ตาม specs ที่ import เข้ามา (ในที่นี้คือ
sayHello
) - start server gRPC server พร้อมระบุ port
- register function ของ RPC เข้า gRPC
และนี่คือ code main.go
ในส่วนของ gRPC Server
package main
import ( "context" "log" "net"
"google.golang.org/grpc" pb "grpc-hello-world/grpc-hello-world/proto")
const ( // Port ที่ server port = ":50051")
// Struct server ใช้ในการ implement gRPC servicestype server struct { // Embedding UnimplementedGreeterServer เพื่อให้มั่นใจว่า struct นี้ implement ทุก method ของ Greeter service pb.UnimplementedGreeterServer}
// Implement SayHello method ที่จะตอบกลับคำทักทาย// รับ Context และ HelloRequest เป็น input และส่งกลับ HelloReply หรือ errorfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { // Log ชื่อที่ได้รับจาก request log.Printf("Received: %v", in.GetName())
// ตอบกลับข้อความทักทาย return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil}
func main() { // สร้าง TCP listener ที่ฟังการเชื่อมต่อบน port ที่กำหนด lis, err := net.Listen("tcp", port) if err != nil { // ถ้าเกิดข้อผิดพลาดขณะสร้าง listener, ให้ log error แล้วออกจากโปรแกรม log.Fatalf("failed to listen: %v", err) }
// สร้าง gRPC server instance ใหม่ grpcServer := grpc.NewServer()
// ลงทะเบียน Greeter service กับ server pb.RegisterGreeterServer(grpcServer, &server{})
// Log ว่า server กำลังฟังการเชื่อมต่อบน port ที่กำหนด log.Printf("Server is listening on port %v", port)
// เริ่มฟังการเชื่อมต่อและให้บริการ gRPC request if err := grpcServer.Serve(lis); err != nil { // ถ้าเกิดข้อผิดพลาดขณะให้บริการ, ให้ log error แล้วออกจากโปรแกรม log.Fatalf("failed to serve: %v", err) }}
ซึ่งจะเห็นว่า ส่วนที่เรามีการ import เข้ามานั้นเป็นส่วนที่เกิดขึ้นจากการ generate ของ protoc ออกมาหมด ดังนั้น ทำให้เรามั่นใจได้ว่า เราจะ implement ตาม specs ของ protobuf ถูกต้องแน่นอนเช่นกัน
หลังจากเพิ่มเรียบร้อย ทำการ run gRPC server ขึ้นมา
go run main.go
ผลลัพธ์การ run server ก็จะทำการ listen อยู่ที่ port localhost:50051 ออกมา

ทีนี้ เราจะมาลองส่งผ่าน gRPC Client กัน โดยหนึ่งใน program ที่สามารถจำลองเป็น gRPC Client ได้นั่นคือ Postman
ทดสอบผ่าน Postman
Ref: https://learning.postman.com/docs/sending-requests/grpc/grpc-request-interface/
โดยในตัว Postman เองนั้น ปัจจุบัน support API Protocol หลากหลายรูปแบบตั้งแต่ HTTP, GraphQL, Socket.IO รวมถึง gRPC ด้วยเช่นกัน โดยสามารถเปลี่ยนจากส่วนของ protocol ใน Postman ได้ตามภาพนี้

หลังจากที่เปลี่ยนเป็น gRPC ให้ทำการระบุตำแหน่งของ gRPC server ซึ่งในทีนี้คือ localhost:50051 ออกมา เสร็จแล้ว ให้ทำการ import .proto
เข้าไป เพื่อระบุ Protobuf spec สำหรับการส่ง (เพราะอย่างที่บอก เราจะส่งข้อมูลไปมาหากันได้ จำเป็นต้องมี Protobuf ที่เหมือนกัน)

เมื่อทำการ import .proto
เข้ามาเรียบร้อย เราจะสามารถเลือก function ตามที่มีอยู่ใน specs ของ Protobuf ได้ให้เราเลือก SayHello
ตาม specs ของ Protobuf

เมื่อเลือกเรียบร้อย เราจะสามารถระบุ message ส่งเข้าไปได้ในรูปแบบของ JSON โดยเราก็สามารถส่งตาม field ของ specs ที่มีการระบุเอาไว้ได้ และเมื่อลองทดสอบส่งข้อมูล (Invoke) ก็จะได้รับ response กลับมา = gRPC server ทำงานได้ปกติ และ implement ตาม specs ของ Protobuf เรียบร้อย

สุดท้าย เราจะลองมา implement gRPC Client ผ่าน Go กัน
gRPC Client
สุดท้าย เราจะลองใช้ Go เป็นส่วนสำหรับเป็น Client ต่อเข้า gRPC Server เข้าไป โดยใน go นั้นก็มี library ที่สามารถต่อเข้าไปได้
ที่ client.go
ทำการต่อเข้า gRPC Server ด้วยคำสั่ง go ตามนี้ เพื่อส่งข้อความ “Mike” เข้าไปยัง SayHello
ที่ gRPC Server ได้สร้างไว้ตาม protobuf
package main
import ( "context" "log" "time"
"google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "grpc-hello-world/grpc-hello-world/proto")
const ( address = "localhost:50051" defaultName = "world")
func main() { // Set up a connection to the server. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel()
creds := insecure.NewCredentials()
// Create a new gRPC client connection (ต่อไปยังที่เดียวกับ gRPC server) conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("Failed to create client: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn)
// Contact the server and print out its response. name := "Mike" r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage())}
ลองดูผลลัพธ์ด้วยการ run คำสั่ง
go run client.go
ผลลัพธ์ ฝั่ง client ก็จะปรากฎว่าส่งข้อมูลไปได้ และได้ข้อมูลกลับมา (เหมือนกับเวลาที่เรายิงผ่าน postman)

ฝั่ง server ก็จะแสดง log ออกมาว่าได้รับข้อมูลจาก gRPC Client แล้วเรียบร้อย

ทีนี้ก็จะขึ้นอยู่กับ Client และว่า เราอยากให้ตัวไหนสำหรับเป็น Client ส่งเข้า gRPC Server (เหมือนกับตัวอย่างที่อยู่ใน diagram ตอนแรก)
*** บทความเพิ่มเติม** สำหรับส่ง gRPC Client แบบ javascript https://medium.com/@maryanngitonga/server-client-communication-using-grpc-with-a-real-life-example-267a0bcba1a9
สุดท้าย เราจะมาเรียนรู้เรื่องการ handle error เบื้องต้ใน gRPC กัน
Error Handling
เรามาลองเพิ่มอีก 1 function สำหรับรับตัวเลข และมี validation ตัวเลขว่า
- ตัวเลขหลังบวกกันต้องหาร 2 ลงตัวเท่านั้น
- ถ้าหารสองไม่ลงตัว = ให้ throw error message ออกไป
สิ่งแรกที่เราต้องเพิ่มคือ Protobuf Calculator
และ Add
สำหรับเพิ่ม function บวกเลขเข้าไป
syntax = "proto3";
option go_package = "grpc-hello-world/proto";
service Calculator { rpc Add (AddRequest) returns (AddResponse);}
message AddRequest { int32 number1 = 1; int32 number2 = 2;}
message AddResponse { int32 result = 1;}
เสร็จแล้ว run command เพื่อแปลง Protobuf เป็น library ของ Go
protoc --go_out=. --go-grpc_out=. proto/calculator.proto
เสร็จแล้ว update ที่ main.go
(gRPC Server) เพื่อเพิ่ม function add เข้าไป โดยเพิ่ม logic สำหรับการดักว่าหาร 2 ไม่ลงตัวให้ throw error เข้าไป
package main
import ( "context" "log" "net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/status"
pb "grpc-hello-world/grpc-hello-world/proto")
const ( port = ":50051")
type server struct { pb.UnimplementedGreeterServer pb.UnimplementedCalculatorServer // เพิ่ม service Calculator}
// function SayHello สำหรับ service Greeterfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil}
// function Add สำหรับ service Calculatorfunc (s *server) Add(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) { result := in.GetNumber1() + in.GetNumber2()
// ตรวจสอบว่าผลรวมหารด้วย 2 ลงตัวหรือไม่ if result%2 != 0 { // ถ้าผลลัพธ์ไม่ใช่เลขคู่ ให้แสดง error return nil, status.Errorf(codes.InvalidArgument, "ผลรวมต้องเป็นเลขคู่เท่านั้น") }
// ถ้าผลลัพธ์เป็นเลขคู่ ส่งค่าผลลัพธ์กลับไป return &pb.AddResponse{Result: result}, nil}
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) }
grpcServer := grpc.NewServer() pb.RegisterGreeterServer(grpcServer, &server{}) pb.RegisterCalculatorServer(grpcServer, &server{}) // ลงทะเบียน service Calculator เพิ่มเข้าไป log.Printf("Server is listening on port %v", port) if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}
สังเกตว่า วิธี throw error ก็จะเหมือนกับการ throw error ใน Rest API Go Service ทั่วไปเลย แล้วพอเรายิงให้เกิด error (เช่นยิงเคส 10, 21) จากผลการยิง Postman ก็จะแสดง error ออกมา พร้อม error message ตัวนั้นออกมาได้

สำหรับ client.go
ในภาษา Go ก็สามารถ handle error ได้เหมือนวิธี handle error ปกติของภาษา Go ได้เลยเช่นกัน
package main
import ( "context" "log" "time"
"google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" pb "grpc-hello-world/grpc-hello-world/proto")
const ( address = "localhost:50051" defaultName = "world")
func main() { // ตั้งค่า connection ไปยัง server โดยมีการกำหนด timeout เป็น 1 วินาที ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel()
// ตั้งค่า credentials ให้ไม่ใช้การเข้ารหัส (insecure) creds := insecure.NewCredentials()
// สร้าง gRPC client connection (ต่อไปยังที่อยู่ gRPC server) conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("Failed to create client: %v", err) } defer conn.Close()
// สร้าง client สำหรับ service Calculator client := pb.NewCalculatorClient(conn)
// สร้าง request โดยมีตัวเลข 2 ตัวที่ทำให้เกิดข้อผิดพลาด (เช่น 3 + 4 = 7 ซึ่งเป็นเลขคี่) request := &pb.AddRequest{ Number1: 3, Number2: 4, }
// เรียกใช้ method Add เพื่อคำนวณผลรวม response, err := client.Add(ctx, request) if err != nil { // ตรวจสอบว่า error ที่เกิดขึ้นเป็น error ของ gRPC หรือไม่ st, ok := status.FromError(err) if ok { // แสดง code และข้อความ error ของ gRPC log.Printf("gRPC error code: %v, error message: %v", st.Code(), st.Message()) } else { // แสดงข้อความ error ที่ไม่คาดคิด log.Fatalf("Unexpected error: %v", err) } return }
// แสดงผลลัพธ์ถ้าไม่มี error log.Printf("Result: %v", response.GetResult())}
ผลลัพธ์จากการลอง run client.go

เพิ่มเติม: เราสามารถเพิ่ม Log Interceptor เข้าไปใน gRPC Server ได้เพื่อใช้สำหรับการเก็บ Log หรือ Debug log ภายใน gRPC Server ได้ เช่น ตัวอย่าง code นี้ที่เพิ่ม unaryInterceptor
เป็น Interceptor เข้า gRPC Server ไป
package main
import ( "context" "log" "net"
"google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/grpc/metadata"
pb "grpc-hello-world/grpc-hello-world/proto")
const ( port = ":50051")
/* code เหมือนเดิม */
// Unary interceptor สำหรับ log การเรียกใช้งานที่เข้ามาfunc unaryInterceptor( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,) (interface{}, error) { // Log the method name log.Printf("gRPC method: %s", info.FullMethod)
// Log metadata (if any) if md, ok := metadata.FromIncomingContext(ctx); ok { log.Printf("Metadata: %v", md) }
// Log request payload log.Printf("Request: %+v", req)
// Proceed with the handler resp, err := handler(ctx, req)
// Log response or error if err != nil { log.Printf("Error: %v", err) } else { log.Printf("Response: %+v", resp) }
return resp, err}
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) }
// สร้าง gRPC server ใหม่ที่ใช้ unary interceptor grpcServer := grpc.NewServer( grpc.UnaryInterceptor(unaryInterceptor), )
// ลงทะเบียน service Greeter และ Calculator pb.RegisterGreeterServer(grpcServer, &server{}) pb.RegisterCalculatorServer(grpcServer, &server{})
log.Printf("Server is listening on port %v", port) if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}
ผลลัพธ์ Log ตอนที่มีการยิง gRPC เข้ามา

นี่คือวิธีการ implement gRPC ทั้งหมดโดยประมาณ
API Gateway Practice

API Gateway เป็น 1 ใน Practice ที่มักจะใช้ร่วมกับ gRPC มักถูกออกแบบมาเพื่อให้บริการสื่อสารระหว่างระบบที่ใช้ gRPC กับบริการอื่น ๆ หรือผู้ใช้งานภายนอก โดยที่ API Gateway จะทำหน้าที่เป็นจุดกลางในการรับ requests และ responses ไปยังบริการ backend หลายๆ Service
โดยหลักการทั่วไปในการใช้งาน API Gateway กับ gRPC มีดังนี้
- Protocol Translation
- API Gateway สามารถแปลงการเรียกใช้ที่มาจากโปรโตคอลอื่น (เช่น HTTP/JSON) ให้กลายเป็น gRPC และส่งต่อไปยัง backend ที่ใช้ gRPC อยู่ ตัวอย่างเช่น ผู้ใช้ภายนอกอาจจะส่งคำขอ HTTP/REST ที่ใช้ JSON แต่ภายในระบบ backend เป็น gRPC API ดังนั้น API Gateway จะทำการแปลงคำขอให้เหมาะสม
- HTTP/1.1 → gRPC (HTTP/2): API Gateway ทำการแปลงการเรียกใช้จาก HTTP/1.1 ไปเป็น gRPC บน HTTP/2 เพื่อให้สามารถสื่อสารกับบริการที่อยู่ในระบบได้
- REST → gRPC: แปลงรูปแบบการสื่อสาร REST ไปเป็น gRPC (Protobuf) ซึ่งใช้สำหรับสื่อสารที่มีประสิทธิภาพมากกว่า
ตัวอย่าง Rest API

ตัวอย่าง GraphQL

- Centralized Authentication and Authorization API Gateway มักทำหน้าที่เป็นจุดควบคุมการตรวจสอบสิทธิ์ (Authentication) และการกำหนดสิทธิ์ (Authorization) สำหรับบริการทั้งหมดที่อยู่เบื้องหลัง ซึ่งช่วยให้การตรวจสอบผู้ใช้งานเป็นมาตรฐานเดียวกันในทุกบริการ
- Traffic Management API Gateway จะช่วยในการจัดการปริมาณการใช้งาน (traffic management) เช่น การทำ Rate Limiting, การควบคุมทรัพยากร และการกระจายโหลด (Load Balancing) เพื่อให้ backend gRPC สามารถรองรับคำขอได้อย่างมีประสิทธิภาพ
- Security Layer API Gateway สามารถจัดการเรื่องความปลอดภัยได้หลายรูปแบบ เช่น TLS termination หรือการจัดการ CORS (Cross-Origin Resource Sharing) สำหรับบริการที่อยู่ด้านหลัง โดยทำให้ gRPC services ไม่ต้องจัดการเรื่องนี้เอง
โดยเครื่องมือที่มักจะเป็นที่นิยมใช้ทำ API Gateway
- Envoy: Proxy ที่ใช้สำหรับ gRPC ซึ่งรองรับฟีเจอร์ต่าง ๆ เช่น Load Balancing, Rate Limiting, Tracing และการแปลง gRPC-Web
- Kong: API Gateway ที่รองรับทั้ง gRPC และ gRPC-Web และมีระบบปลั๊กอินที่ยืดหยุ่นในการทำงานร่วมกับฟีเจอร์อื่น ๆ
- Nginx: ใช้เป็น API Gateway สำหรับการ proxy ไปยัง gRPC services โดยรองรับ HTTP/2 และ gRPC-Web
ซึ่งโจทย์สำคัญของ API Gateway นั้นคือตัวช่วยให้การจัดการการสื่อสารระหว่างผู้ใช้งานภายนอกหรือบริการอื่น ๆ กับ gRPC services มีความยืดหยุ่นและปลอดภัยมากขึ้นนั่นเอง
สามารถอ่านเพิ่มเติมตาม Reference เหล่านี้ได้
- https://www.koyeb.com/tutorials/build-a-grpc-api-using-go-and-grpc-gateway
- https://adevait.com/go/transcoding-of-http-json-to-grpc-using-go
- https://tailcall.run/docs/graphql-grpc-tailcall/
สรุปทั้งหมด
gRPC กับ Go เป็นการผสานกันที่ลงตัวสำหรับการสร้างระบบที่ต้องการประสิทธิภาพสูงและสามารถสื่อสารกันได้อย่างรวดเร็ว gRPC ใช้ Protocol Buffers ในการจัดการข้อมูล ซึ่งทำให้การส่งข้อมูลระหว่าง Service มีขนาดเล็กลงและรวดเร็วกว่า JSON หรือ XML Go เองเป็นภาษาที่ออกแบบมาเพื่อรองรับงานที่เกี่ยวข้องกับระบบ network ทำให้การเขียน gRPC ด้วย Go มีความเรียบง่ายและทรงประสิทธิภาพ
การใช้งาน gRPC กับ Go ยังช่วยให้ระบบสามารถออกแบบเป็นแบบ microservices ได้ง่ายขึ้น โดยสามารถแยกส่วนประกอบของระบบออกจากกัน แต่ยังสามารถสื่อสารได้อย่างมีประสิทธิภาพผ่าน API ที่กำหนดอย่างชัดเจน นอกจากนี้ gRPC ยังรองรับฟีเจอร์ที่สำคัญ เช่น การ streaming ข้อมูลและการตรวจสอบความปลอดภัย ทำให้ Go และ gRPC เหมาะสำหรับการสร้างระบบขนาดใหญ่ที่ต้องการการขยายตัว
สุดท้าย การนำ gRPC มาใช้งานร่วมกับ Go ยังช่วยเพิ่มความยืดหยุ่นในการพัฒนาและขยายระบบ นอกจากนี้ gRPC ยังสามารถทำงานร่วมกับเทคโนโลยีอื่น ๆ ได้ดี เช่น API Gateway หรือ Kubernetes ซึ่งทำให้ระบบสามารถรองรับปริมาณการใช้งานจำนวนมากได้ โดยที่ยังรักษาประสิทธิภาพและความปลอดภัย
หวังว่าบทความนี้จะช่วยทำให้ทุกคนรู้จัก gRPC มากขึ้นนะครับ 😁
Footnotes
- มาเรียนรู้พื้นฐาน Diagram แต่ละประเภทของงาน development กันมี Video
รู้จักกับ Diagram ที่ใช้บ่อยประเภทต่างๆว่ามีอะไรบ้าง และใช้สำหรับทำอะไรบ้าง
- มารู้จักกับ Elasticseach ที่ใช้ทำ Search engine กันมี Video
มาลองทำ search ผ่าน Elasticsearch กัน มาทำความรู้จักกันว่า Elasticsearch คืออะไร ?
- ลอง Firebase Data Connectมี Github
มารู้จัก นวัตกรรม SQL จากฝั่ง Firebase ผ่าน Service ตัวใหม่ Firebase Data Connect กัน
- มาแก้ปัญหา Firestore กับปัญหาราคา Read pricing สุดจี๊ดมี Video มี Github
ในฐานะที่เป็นผู้ใช้ Firebase เหมือนกัน เรามาลองชวนคุยกันดีกว่า ว่าเราจะสามารถหาวิธีลด Pricing หรือจำนวนการ read ของ Firestore ได้ยังไงกันบ้าง