มารู้จักกับ gRPC และ Go กัน

/ 15 min read

Last Updated:

Share on social media

grpc-go สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

รู้จักกับ gRPC

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

grpc-1.webp

ภาพจาก 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

https://grpc.io/img/landing-2.svg

ใน 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 องค์ประกอบใหญ่ๆคือ

  1. 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

  1. 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 เท่านั้น

  1. 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

grpc-2.webp

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 คือ

  1. กำหนดโครงสร้างข้อมูลในไฟล์ .proto
  2. compile ไฟล์ .proto ด้วยเครื่องมือ protoc เพื่อสร้าง code ที่สามารถ serialize และ deserialize ข้อมูลได้
  3. ใช้งาน 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
person = Person(name="John Doe", id=123, email="[email protected]")
# แปลงเป็น binary เพื่อส่งข้อมูล
binary_data = person.SerializeToString()
# การรับข้อมูลและแปลงกลับเป็น object
new_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 ที่เราต้องทำก่อนว่าเราต้องทำอะไรบ้าง โดยสิ่งที่เราจะต้องทำคือ

  1. กำหนด 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;
}
  1. compile file .proto โดยใช้ protoc (Protocol Buffers compiler) เพื่อแปลงไฟล์ .proto เป็น code stub สำหรับการใช้งานในภาษาโปรแกรมที่เราจะใช้งาน เช่น Go, Python, Java เป็นต้น (ซึ่งในทีนี้เราจะใช้ภาษา Go กัน)
Terminal window
# ตัวอย่างคำสั่งที่ใช้ compile protoc
protoc --go_out=. --go-grpc_out=. path/to/your.proto
  1. Implement gRPC Server โดยเขียน code ที่ทำหน้าที่เป็น gRPC Server โดย implement service ที่กำหนดในไฟล์ .proto และสร้างเป็น server gRPC Server ขึ้นมา
type server struct {
pb.UnimplementedUserServiceServer
}
// implement GetUser ตามที่ตกลงไว้ใน .proto
func (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)
}
}
  1. 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

Terminal window
# init project ด้วย
go mod init <ชื่อ project>
# example
go mod init grpc-hello-world

และทำการลง library ที่เกี่ยวข้องกับ gRPC ของ go

Terminal window
go get google.golang.org/grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
  • grpc เป็น gRPC library สำหรับ Go ที่ใช้ในการสร้าง client และ server ที่สื่อสารผ่าน protocal gRPC
  • protoc-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 ชื่อ Greeter
service 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 ออกมา

โดยคำสั่งที่เราจะใช้คือ

Terminal window
# สำหรับชาว Linux / Mac ที่อาจจะใช้ไม่ได้ เนื่องจาก protoc ยังไม่ได้ระบุ GOPATH ที่อ้างอิงให้มาใช้งานคำสั่ง protoc ได้
# สามารถใช้คำสั่งนี้ได้
export PATH="$PATH:$(go env GOPATH)/bin"
# คำสั่งสำหรับ compile protobuf
protoc --go_out=. --go-grpc_out=. proto/helloworld.proto

เราก็จะได้ ผลลัพธ์ออกมาเป็น go file ที่ทำการ implement ตาม .proto เป็นที่เรียบร้อย

grpc-3.webp

ตอนนี้ protobuf เราก็พร้อมสำหรับการนำไป implement ทั้งฝั่ง gRPC Server และ gRPC client และ step ต่อมาเราจะเริ่ม implement gRPC Server กัน เพื่อใส่ logic ให้กับ sayHello ตามที่กำหนดไว้ใน protobuf กัน

gRPC Server

Step ถัดมา เราจะเริ่ม implement function sayHello ในส่วนของ logic กัน โดยสิ่งที่ gRPC Server ต้องทำคือ

  1. ทำการ import library (ที่ผ่าน compile protobuf มา) นำเข้ามา implement ที่ go
  2. implement function ตาม specs ที่ import เข้ามา (ในที่นี้คือ sayHello)
  3. start server gRPC server พร้อมระบุ port
  4. 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 services
type server struct {
// Embedding UnimplementedGreeterServer เพื่อให้มั่นใจว่า struct นี้ implement ทุก method ของ Greeter service
pb.UnimplementedGreeterServer
}
// Implement SayHello method ที่จะตอบกลับคำทักทาย
// รับ Context และ HelloRequest เป็น input และส่งกลับ HelloReply หรือ error
func (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 ขึ้นมา

Terminal window
go run main.go

ผลลัพธ์การ run server ก็จะทำการ listen อยู่ที่ port localhost:50051 ออกมา

grpc-4.webp

ทีนี้ เราจะมาลองส่งผ่าน 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 ได้ตามภาพนี้

postman-grpc-1.webp

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

postman-grpc-2.webp

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

postman-grpc-3.webp

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

postman-grpc-4.webp

สุดท้าย เราจะลองมา 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 คำสั่ง

Terminal window
go run client.go

ผลลัพธ์ ฝั่ง client ก็จะปรากฎว่าส่งข้อมูลไปได้ และได้ข้อมูลกลับมา (เหมือนกับเวลาที่เรายิงผ่าน postman)

postman-grpc-5.webp

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

postman-grpc-6.webp

ทีนี้ก็จะขึ้นอยู่กับ 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

Terminal window
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 Greeter
func (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 Calculator
func (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 ตัวนั้นออกมาได้

postman-grpc-7.webp

สำหรับ 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

postman-grpc-8.webp

เพิ่มเติม: เราสามารถเพิ่ม 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 เข้ามา

postman-grpc-9.webp

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

API Gateway Practice

gateway-proxying.webp

API Gateway เป็น 1 ใน Practice ที่มักจะใช้ร่วมกับ gRPC มักถูกออกแบบมาเพื่อให้บริการสื่อสารระหว่างระบบที่ใช้ gRPC กับบริการอื่น ๆ หรือผู้ใช้งานภายนอก โดยที่ API Gateway จะทำหน้าที่เป็นจุดกลางในการรับ requests และ responses ไปยังบริการ backend หลายๆ Service

โดยหลักการทั่วไปในการใช้งาน API Gateway กับ gRPC มีดังนี้

  1. 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

go-1.webp

ตัวอย่าง GraphQL

graphql_on_grpc.webp
  1. Centralized Authentication and Authorization API Gateway มักทำหน้าที่เป็นจุดควบคุมการตรวจสอบสิทธิ์ (Authentication) และการกำหนดสิทธิ์ (Authorization) สำหรับบริการทั้งหมดที่อยู่เบื้องหลัง ซึ่งช่วยให้การตรวจสอบผู้ใช้งานเป็นมาตรฐานเดียวกันในทุกบริการ
  2. Traffic Management API Gateway จะช่วยในการจัดการปริมาณการใช้งาน (traffic management) เช่น การทำ Rate Limiting, การควบคุมทรัพยากร และการกระจายโหลด (Load Balancing) เพื่อให้ backend gRPC สามารถรองรับคำขอได้อย่างมีประสิทธิภาพ
  3. 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 เหล่านี้ได้

สรุปทั้งหมด

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

  1. https://grpc.io/

  2. https://trends.stackoverflow.co/?tags=rest,graphql,grpc,websocket,openai-api

  3. https://grpc.io/docs/what-is-grpc/introduction/


Related Post

Share on social media