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.

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:

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 —
RunningSumgử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ề
- gRPC có bốn kiểu RPC, bật bằng từ khoá
streamtrong.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. - 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).
- 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
- gRPC — Core concepts: RPC life cycle (4 loại): https://grpc.io/docs/what-is-grpc/core-concepts/
- gRPC — Basics tutorial (Go): streaming: https://grpc.io/docs/languages/go/basics/
- gRPC — Performance best practices (streaming vs unary): https://grpc.io/docs/guides/performance/
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.