มาทำ gRPC Service ด้วย Go กัน

ทำไมถึง gRPC

โดยปกติ microservice มักจะสื่อสารกันด้วย REST API ผ่านทาง HTTP 1.1 แต่เอ HTTP/2 นี่เขาว่ามันดีนัก ถ้าจะให้ microservice ของเราสื่อสารกันผ่าน HTTP/2 แทนจะทำไงดีล่ะ

ก็ใช้ gRPC สิ gRPC คือ RPC (Remote Procedure Call) framework บน HTTP/2 ทำให้ client และ server สามารถสื่อสารกันโดยอาศัยคุณสมบัติของ HTTP/2 และก้าวข้ามข้อจำกัดของ HTTP 1.1 ได้

Bonus: บทความดีๆเกี่ยวกับ HTTP/2


รู้จักกับ Protobuf

https://developers.google.com/protocol-buffers/

Protobuf ย่อมาจาก Protocol Buffers จะทำหน้าที่เหมือน schema สำหรับ data structure ที่ใช้สื่อสารกันระหว่าง client และ server จะมองว่ามันคือ WSDL ก็ได้

ซึ่ง gRPC ใช้ Protobuf ในการอธิบายว่า service นั้นมี RPC อะไรบ้างและแต่ละ procedure มีการสื่อสารด้วย message หน้าตาแบบไหน

ความพิเศษของ Protobuf คือ เราสามารถจะ compile schema ในรูปของ file .proto เพื่อสร้าง library สำหรับแต่ละภาษาได้เลย เท่านี้เราก็ไม่ต้องมา maintain SDK สำหรับ client ของเราตั้งไม่รู้กี่ภาษาแล้ว เย่!

โยน file protobuf ใส่ service consumer แล้วให้เขาไป generate SDK เองเล้ย!!

โดย Protobuf ประกอบด้วย

  1. file proto
  2. Protobuf Compiler (protoc)

file proto

ตัวอย่าง pingpong.proto

สิ่งที่ pingpong.proto นี้บอกคือ

  1. มี message ชื่อ Ping และ Pong
  2. service ชื่อ PingPong นี้มี rpc method ชื่อ StartPing
  3. StartPing สื่อสารกันแบบ unary โดย client จะส่ง message Ping และ server จะส่ง Pong กลับไป

reference: https://developers.google.com/protocol-buffers/docs/proto3

ทำความเข้าใจ message

ตัวอย่าง message

message Contact {    
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
    message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
    int32 id = 1;
string first_name = 2;
string last_name = 3;
repeated PhoneNumber phones = 4;
}

message Contact บอกว่า Contact มี attribute

  1. id
  2. first_name
  3. last_name
  4. phones ซึ่งเป็น array ของ PhoneNumber (รองรับ nested message ด้วย)
  5. PhoneNumber มี 3 ประเภท (รองรับ enum ด้วย เริ่มจาก 0)

ทำไม int32 id ถึง = 1?

เลข 1 คือ field number โดย field 1–15 จะใช้ 1 byte ในการ encode ขณะที่ field 16–2047 ใช้ 2 byte ดังนั้น field 1–15 จึงควรจะใช้สำหรับ field ที่เป็น required field

การสื่อสารผ่าน rpc มี 4 แบบ

service SomeService {    
// PingPongService has a method, which is StartPing
rpc Unary (Ping) returns (Pong);
rpc ClientStream(stream Ping) returns (Pong);
rpc ServerStream(Ping) returns (stream Pong);
rpc BidirectionalStream(stream Ping) returns (Pong);
}

client และ server สามารถสื่อสารผ่าน rpc ได้ 4 แบบ ได้แก่

Unary คือ client ส่ง request มา server ก็ส่ง response ไป คล้ายๆกับ http request ปกติ

rpc Unary(Ping) returns (Pong);

Client-side streaming แบบนี้ client จะส่ง message ให้ server เป็น stream และ server จะ response ครั้งเดียวเมื่อ client หยุดส่ง message (server มีมารยาท ไม่พูดแทรก)

rpc ClientStream(stream Ping) returns (Pong);

Server-side streaming แบบนี้ client ส่ง message ไปให้ server ส่ง stream กลับมา

rpc ServerStream(Ping) returns (stream Pong);

Bidirectional streaming ส่ง stream ใส่กันทั้ง 2 ฝ่าย ใครอยากส่งเมื่อไร ตอนไหนพูดแทรกได้เต็มที่

rpc BidirectionalStream(stream Ping) returns (stream Pong);

https://github.com/varshard/helloproto/tree/master/bidirectionalstream


compile proto เป็น code ด้วย Protoc

เริ่มจากติดตั้ง protoc (Protocol Compiler) สำหรับ compile file .proto ก่อน

Note: ถ้าใครใช้ package manager ลง protoc ด้วย package manager แล้วข้ามไปที่วิธี compile proto เลย

โดยไป download protoc ตาม os ของเราจาก https://github.com/protocolbuffers/protobuf/releases ได้เลย จากนั้น extract zip ออกมาไว้สักที่จะได้ content ของ zip ได้แก่ bin, include และ readme.txt โดยให้เราเอา path ของ bin ไปใส่ไว้ใน $PATH variable

หลังจากนั้นลองพิมพ์ command

$ protoc --version

ถ้าแสดงเลข version ออกมาก็เป็นอันเรียบร้อย

จากนั้นเราก็ลง plugin ภาษา Go ให้กับ protoc โดย execute command นี้

go get -u github.com/golang/protobuf/protoc-gen-go

เท่านี้เราก็พร้อมจะ compile proto ได้

file proto ที่จะใช้ในตัวอย่างนี้

proto file that we are going to compile

protoc พร้อมแล้ว proto file ก็พร้อมแล้ว เราก็มา generate code ภาษา Go จาก pingpong.proto กันเลยดีกว่า โดยพิมพ์คำสั่ง

protoc --go_out=plugins=grpc:<relative_output_path> pingpong.proto

เท่านี้เราก็จะได้ file pingpong.pb.go ออกมายัง relative_output_path ของเราแล้ว

Note: protoc จะไม่สร้าง directory ให้เรา เช่นถ้า relative_output_path คือ generated/proto เราก็ต้องสร้าง directory generated และ proto เอง

pingpong.pb.go มีอะไรบ้าง

  1. struct ของ message ต่างๆ
  2. pingPongClient คือ struct client ที่ใช้สื่อสารกับ server
  3. PingPongServer คือ interface ของตัว server เอง

พร้อมแล้ว เริ่ม implement กันดีกว่า

อ่านเพิ่มเติม https://www.grpc.io/docs/tutorials/basic/go/

ในตัวอย่างนี้คือการ implement gRPC service จาก pingpong.proto ที่กล่าวถึงด้านบน

มา implement ฝั่ง server กันก่อน

PingPong server implementation

ไม่รอช้า เริ่มจาก implement PingPongServer ก่อนเลย ด้วย PingPongServerImpl หลังจากนั้นก็ start grpc server ที่ localhost:9000

จากนั้น compile และ run server ได้เลย

มาต่อกันที่ client

PingPong client /

คราวนี้ proto ช่วย implement PingPongClient ให้แล้ว เราแค่สร้าง connection ไปที่ localhost:9000 (StartPingPongClient) แล้วเริ่ม ping (SendPing) ได้เลย

server พร้อม

client พร้อม

เท่านี้เราแค่ run server ขึ้นมาแล้ว ให้ client ยิง ping เล่นได้เลย

ตัวอย่างเต็มๆดูได้ที่ https://github.com/varshard/helloproto/tree/master/unarypingpong

หรือตัวอย่างล้ำๆที่ https://github.com/grpc/grpc-go/tree/master/examples