REST về cơ bản chỉ có một mô hình giao tiếp: client gửi một request, server trả một response, hết. Muốn nhận nhiều kết quả thì phải phân trang hoặc polling; muốn đẩy dữ liệu thì phải dùng WebSocket riêng. gRPC đi xa hơn: nó có bốn kiểu RPC ngay trong lõi, và chuyển giữa chúng chỉ bằng một từ khoá — stream — trong file .proto. Bốn kiểu này không phải "tính năng hay ho" mà là bốn mô hình luồng dữ liệu khác nhau, mỗi cái hợp một bài toán. Hiểu và chọn đúng kiểu là một quyết định thiết kế API thật sự. Bài này (phần 3 loạt gRPC nâng cao) chạy thật cả bốn trong một service để thấy chúng khác nhau thế nào.
Bốn kiểu, một từ khoá: stream
Đặt stream ở phía request, phía response, hoặc cả hai, ta được bốn tổ hợp:
- Unary (không
stream): 1 request → 1 response. Giống REST — request-response thông thường. - Server streaming (
streamở response): 1 request → N response. Server đẩy nhiều kết quả qua một kết nối (feed, danh sách lớn, tiến trình). - Client streaming (
streamở request): N request → 1 response. Client gửi nhiều, server gộp lại trả một kết quả (upload, tổng hợp). - Bidirectional streaming (
streamcả hai): N ↔ N, hai bên gửi/nhận xen kẽ độc lập (chat, realtime, trò chơi).
service Shop {
rpc GetOrder(OrderReq) returns (Order); // unary
rpc ListOrders(OrderReq) returns (stream Order); // server streaming
rpc SumAmounts(stream Order) returns (Sum); // client streaming
rpc Chat(stream Msg) returns (stream Msg); // bidirectional
}
Phía Go, streaming dùng Send()/Recv() lặp cho tới io.EOF, thay vì một lời gọi trả giá trị:
// server streaming: gọi Send() nhiều lần
for i:=1; i<=5; i++ { stream.Send(&Order{...}) }
// client streaming: Recv() tới EOF rồi SendAndClose()
for { o,err := stream.Recv(); if err==io.EOF { ... } }

Hình 1: Bốn kiểu RPC khai bằng stream trong .proto — ở request, response, hoặc cả hai. Phía Go, streaming dùng Send()/Recv() lặp tới io.EOF thay vì một lời gọi trả giá trị đơn.
Đo thật: chạy cả bốn kiểu
Mình cài đặt cả bốn RPC trong một service Shop trên go-lab, chạy server + client trong cùng process, và đo số message mỗi chiều:

Hình 2: Kết quả thật. (1) Unary: gửi 1 req (id=7) → nhận 1 resp (amount=100). (2) Server streaming: gửi 1 req → nhận 5 resp qua stream. (3) Client streaming: gửi 4 req (50+100+150+200) → nhận 1 resp với total=500, count=4. (4) Bidirectional: gửi 3 msg → nhận 3 msg xen kẽ hai chiều.
Đọc kết quả:
- Unary — 1 req → 1 resp: giống REST. Client gọi
GetOrder(id=7), nhận mộtOrder. Đơn giản, đồng bộ, dễ hiểu — phù hợp đại đa số API (lấy/tạo/sửa một thứ). - Server streaming — 1 req → 5 resp: client gửi một request rồi lặp
Recv()nhận 5 kết quả. Khác phân trang REST ở chỗ: một kết nối, server đẩy liên tục, client xử lý ngay khi nhận từng cái (không chờ hết). Hợp cho tải danh sách lớn, feed sự kiện, báo tiến trình. - Client streaming — 4 req → 1 resp (total=500): client lặp
Send()4 order rồiCloseAndRecv()nhận một kết quả gộp. Total = 50+100+150+200 = 500 đúng, count=4 — server tích luỹ qua stream rồi trả một lần. Hợp cho upload (gửi file theo chunk), gộp số liệu. - Bidirectional — 3 ↔ 3: client và server gửi/nhận độc lập, xen kẽ. Client gửi 3 msg, nhận lại 3 msg echo. Hai luồng chạy song song trên một kết nối — điều REST không làm được mà không có WebSocket. Hợp cho chat, cộng tác realtime, trò chơi.
Thông điệp cốt lõi: gRPC cho bạn chọn mô hình luồng khớp với bài toán, thay vì ép mọi thứ vào request-response. Và vì tất cả là stream trong .proto, chuyển đổi chỉ là một dòng khai báo — codegen lo phần còn lại.
Vì sao streaming khác polling/phân trang
Người quen REST hay hỏi: "server streaming khác gì phân trang?". Khác ở một kết nối, đẩy chủ động. Phân trang REST: client gửi N request riêng (?page=1, ?page=2...), mỗi cái một round-trip, server bị động chờ hỏi. Server streaming: một kết nối, server chủ động đẩy khi có dữ liệu, client nhận ngay. Với dữ liệu sinh dần theo thời gian (sự kiện, log, kết quả tính toán lâu), streaming cho độ trễ thấp hơn nhiều và không tốn round-trip. Nhưng với dữ liệu tĩnh có thể nhảy trang (duyệt sản phẩm), phân trang linh hoạt hơn (nhảy trang 5, quay lại trang 2). Chọn theo bản chất dữ liệu: sinh dần → stream, tĩnh/nhảy được → phân trang.
Đánh đổi cần cân nhắc
Streaming phức tạp hơn unary — chỉ dùng khi thật cần. Unary là một lời gọi hàm: dễ viết, dễ test, dễ retry (bài sau). Streaming cần quản lý vòng đời stream, xử lý io.EOF, lỗi giữa chừng, goroutine cho bidi (như demo dùng channel done). Nhiều lỗi tinh vi đến từ streaming: quên CloseSend(), deadlock khi cả hai cùng chờ nhận. Mặc định dùng unary; chỉ nâng lên streaming khi mô hình dữ liệu thật sự cần (nhiều kết quả sinh dần, hai chiều realtime).
Retry khó hơn với streaming. Unary RPC retry đơn giản: gọi lại. Nhưng một stream đang dở (đã gửi 3/10 message rồi lỗi) không retry sạch được — gửi lại từ đầu có thể gây trùng, tiếp tục giữa chừng cần server hỗ trợ resume. Streaming đánh đổi tính dễ phục hồi lấy throughput và realtime. Với thao tác quan trọng cần đảm bảo, cân nhắc unary + idempotency (như loạt Message Queue) thay vì streaming.
Bidirectional dễ gây deadlock nếu thiết kế sai. Trong bidi, nếu cả client và server cùng chờ nhận trước khi gửi, chúng khoá chết nhau. Demo tránh điều này bằng cách tách việc nhận ra một goroutine riêng. Luồng gửi/nhận phải thiết kế để không bao giờ cả hai cùng chờ — đây là lỗi kinh điển của bidi streaming, và là lý do nó là kiểu khó dùng đúng nhất trong bốn kiểu.
Ba ý mang về
- Bốn kiểu RPC mở khoá bằng
stream: đo thật unary (1→1), server streaming (1→5), client streaming (4→1 với total=500 đúng), bidirectional (3↔3) — chuyển đổi chỉ bằng đặtstreamở request/response trong.proto. - Mỗi kiểu hợp một luồng dữ liệu: unary cho request-response thường, server-stream cho tải danh sách/feed sinh dần, client-stream cho upload/gộp, bidi cho chat/realtime — chọn theo bản chất dữ liệu, không ép mọi thứ vào một mô hình.
- Streaming mạnh nhưng phức tạp hơn — dùng đúng liều: unary dễ viết/test/retry; streaming cần quản lý vòng đời + io.EOF, khó retry sạch, và bidi dễ deadlock nếu cả hai cùng chờ — mặc định unary, chỉ nâng lên streaming khi thật cần.
Nguồn
- gRPC docs — Core concepts: RPC life cycle: https://grpc.io/docs/what-is-grpc/core-concepts/
- gRPC Go — Basics tutorial (streaming): https://grpc.io/docs/languages/go/basics/
- Protocol Buffers — Defining services: https://protobuf.dev/programming-guides/proto3/#services
Phần sau ta đào sâu server streaming thực chiến: đẩy nhiều response qua một kết nối, đo khác biệt so với trả một lần và khi nào streaming thực sự thắng.