Một trong những khác biệt lớn nhất giữa gRPC và REST ít được nói tới cho tới khi bạn cần nó: REST về bản chất chỉ có một kiểu tương tác — một request, một response. Muốn server đẩy nhiều kết quả liên tục, hay client gửi một luồng dữ liệu, bạn phải tự dựng (SSE, WebSocket, chunked transfer) với đủ thứ xử lý tay. gRPC cho sẵn bốn kiểu RPC nhờ nền HTTP/2 hỗ trợ nhiều luồng hai chiều trên một kết nối — và bật chúng chỉ cần thêm một từ khoá stream vào .proto. Bài này (phần 3/12) chạy thật cả bốn trong go-lab và chỉ rõ khi nào dùng kiểu nào.

Bốn kiểu, khai báo bằng một từ khoá

Trong .proto, vị trí của từ khoá stream (ở tham số, ở kết quả, hay cả hai) quyết định kiểu RPC:

rpc Double(Num) returns (Num);                   // unary
rpc CountUp(Num) returns (stream Num);           // server streaming
rpc SumAll(stream Num) returns (Sum);            // client streaming
rpc RunningSum(stream Num) returns (stream Sum); // bidirectional

protoc sinh ra API tương ứng: unary trả thẳng kết quả, còn ba kiểu streaming trả về một stream object để Send()/Recv() nhiều lần.

Ảnh chụp đoạn mã nền tối minh hoạ bốn kiểu RPC của gRPC một thứ REST không có sẵn, nhờ HTTP/2 gRPC hỗ trợ streaming hai chiều ngay trong contract chỉ cần thêm từ khoá stream vào proto. Khai báo trong proto thêm stream là có streaming rpc Double Num returns Num unary rpc CountUp Num returns stream Num server streaming rpc SumAll stream Num returns Sum client streaming rpc RunningSum stream Num returns stream Sum bidirectional. Bốn kiểu và khi nào dùng, một Unary client 1 req tới server client 1 resp từ server request-response thường CRUD query lẻ, hai Server streaming client 1 req tới server client N resp từ server trả nhiều kết quả feed tải danh sách lớn tiến độ, ba Client streaming client N req tới server client 1 resp từ server upload gộp đẩy nhiều bản ghi tải file theo chunk, bốn Bidirectional client N hai chiều server xen kẽ realtime hai chiều chat game đồng bộ liên tục

Hình 1: Bốn kiểu RPC — vị trí từ khoá stream trong .proto quyết định kiểu. Unary (1→1), server streaming (1→N), client streaming (N→1), bidirectional (N↔N). Mỗi kiểu hợp một loại bài toán khác nhau.

Đo thật: chạy cả bốn

Mình dựng một service Calc với đủ bốn RPC và chạy thật từng cái:

Ảnh chụp bảng kết quả đo thật chạy cả bốn kiểu RPC output thật go-lab grpc-go v1.67.1 service Calc 4 RPC. Một Unary 1 request 1 response Double 21 trả 42 request-response cổ điển client gọi chờ nhận đúng một kết quả. Hai Server streaming 1 request N response CountUp 5 nhận 5 message 1 2 3 4 5 server đẩy liên tiếp nhiều message cho một yêu cầu feed tải danh sách lớn theo lô báo tiến độ. Ba Client streaming N request 1 response SumAll 10 20 30 40 gửi 4 nhận 1 total 100 count 4 client đẩy nhiều message rồi nhận một kết quả tổng hợp upload theo chunk gộp số liệu. Bốn Bidirectional N request N response xen kẽ RunningSum gửi 5 15 25 nhận total 5 total 20 total 45 ba lần running sum hai chiều độc lập cùng lúc trên một kết nối chat game realtime đồng bộ liên tục mỗi lần client gửi server trả ngay tổng tích luỹ

Hình 2: Đo thật trong go-lab. Unary: Double(21)→42. Server streaming: CountUp(5)→5 message [1 2 3 4 5]. Client streaming: SumAll(10,20,30,40)→total=100, count=4. Bidirectional: RunningSum gửi 5,15,25 nhận 3 running sum (5, 20, 45).

Kết quả thật từng kiểu, kèm khi nào dùng:

  • ① Unary — Double(21) → 42. Một request, một response: y hệt REST. Dùng cho đại đa số trường hợp — CRUD, query lẻ, lệnh đơn. Đừng dùng streaming khi unary là đủ.
  • ② Server streaming — CountUp(5) → [1,2,3,4,5] (5 message). Server đẩy nhiều kết quả cho một yêu cầu. Hợp khi kết quả lớn hoặc đến dần: tải danh sách khổng lồ theo lô (không dồn hết vào một response), feed sự kiện, báo tiến độ một tác vụ dài.
  • ③ Client streaming — SumAll(10,20,30,40) → total=100, count=4. Client gửi nhiều message rồi nhận một kết quả tổng hợp. Hợp cho upload theo chunk (tải file lớn), gộp/tính tổng một luồng số liệu, ghi hàng loạt.
  • ④ Bidirectional — RunningSum gửi 5,15,25 và nhận lại 3 running sum (5, 20, 45) xen kẽ. Hai chiều độc lập cùng lúc trên một kết nối: mỗi lần client gửi một số, server trả ngay tổng tích luỹ. Hợp cho realtime hai chiều — chat, game, đồng bộ trạng thái liên tục.

Điểm mạnh của bidirectional là hai chiều hoàn toàn độc lập: client không phải chờ response trước khi gửi tiếp, server không phải chờ hết input mới trả lời. Trong demo, go func() nhận trong khi vòng lặp chính vẫn gửi — HTTP/2 ghép cả hai luồng trên một TCP connection.

Đánh đổi cần cân nhắc

Streaming không miễn phí — đừng dùng khi unary đủ. Mỗi stream giữ một luồng HTTP/2 mở, tốn tài nguyên hai phía, và code xử lý Send/Recv/io.EOF phức tạp hơn hẳn một call unary. Nếu bạn chỉ cần một request-response, dùng unary — nó đơn giản hơn, dễ retry hơn, dễ load-balance hơn (bài grpc-10). Streaming chỉ đáng khi bản chất dữ liệu là luồng.

Streaming khó retry và load-balance. Một unary call thất bại thì retry cả call là xong. Nhưng một stream đang chạy giữa chừng mà đứt thì retry phức tạp: gửi lại từ đâu, phần đã xử lý thế nào? Và load balancer khó phân phối lại một stream dài đang mở. Với stream dài, phải tự thiết kế cơ chế checkpoint/resume ở tầng ứng dụng.

Thứ tự trong một stream được đảm bảo, nhưng backpressure cần hiểu. Message trong một stream đến đúng thứ tự (HTTP/2 lo điều đó). Nhưng nếu một bên gửi nhanh hơn bên kia xử lý, flow control của HTTP/2 sẽ chặn lại (backpressure) — bài grpc-09 sẽ đo chuyện này. Viết code streaming phải tính tới việc Send có thể chậm lại khi phía kia bận.

Ba ý mang về

  1. gRPC có bốn kiểu RPC, bật bằng từ khoá stream trong .proto. Unary (1→1), server streaming (1→N), client streaming (N→1), bidirectional (N↔N). Đây là thứ REST không có sẵn — nhờ HTTP/2 cho phép nhiều luồng hai chiều trên một kết nối.
  2. Mỗi kiểu hợp một bài toán. Đo thật: unary cho query lẻ (Double→42); server streaming cho kết quả nhiều/đến dần (CountUp→5 message); client streaming cho upload/gộp (SumAll→total=100); bidirectional cho realtime hai chiều (RunningSum xen kẽ 3 lần).
  3. Streaming mạnh nhưng tốn — dùng đúng chỗ. Nó giữ luồng mở, khó retry và load-balance hơn unary, và cần hiểu backpressure. Khi một request-response là đủ, dùng unary; chỉ streaming khi bản chất dữ liệu thực sự là một luồng.

Nguồn

Phần sau ta quay lại Protobuf nhưng ở góc tiến hoá: vì sao field number là bất biến, thêm/bỏ field thế nào để không vỡ, và đo thật đọc dữ liệu cũ bằng schema mới.