gRPC nhanh không chỉ nhờ protobuf — nó dựa trên HTTP/2, và tính năng cốt lõi của HTTP/2 là multiplexing: nhiều request chạy song song trên một kết nối TCP duy nhất. Đây là bước nhảy lớn so với HTTP/1.1, nơi một kết nối chỉ xử lý một request tại một thời điểm. Hiểu multiplexing giải thích vì sao HTTP/2 (và gRPC trên nó) vượt trội cho giao tiếp nhiều request. Bài này dựng server + client HTTP/2 trong Go, đo thật chênh lệch so với HTTP/1.1, và mổ xẻ cơ chế stream/frame bên dưới.
Vấn đề HTTP/1.1: head-of-line blocking
Một kết nối HTTP/1.1 xử lý đúng một request tại một thời điểm — request sau phải chờ request trước hoàn tất. Đây gọi là head-of-line (HOL) blocking: một request chậm chặn tất cả request phía sau trên cùng kết nối.
[conn] req1 --50ms--> req2 --50ms--> req3 ... (tuần tự)
Trình duyệt bù bằng cách mở nhiều kết nối song song (thường ~6 mỗi host), nhưng mỗi kết nối tốn một lần bắt tay TCP (và TLS), và số lượng bị giới hạn.
HTTP/2: một kết nối, nhiều stream
HTTP/2 chia mỗi request thành một stream có id riêng, và cắt dữ liệu thành các frame (HEADERS, DATA...) đan xen nhau trên cùng một kết nối TCP:
[conn] [s1-frame][s3-frame][s1-frame][s5-frame]... (đan xen)
Các stream chạy đồng thời — không stream nào chặn stream khác. Trong Go, dựng server HTTP/2 chỉ cần bật (thực tế: dùng TLS là net/http tự bật HTTP/2; ở đây dùng h2c — HTTP/2 không TLS — cho demo):
h2s := &http2.Server{}
srv := &http.Server{Addr: ":8099", Handler: h2c.NewHandler(mux, h2s)}

Hình 1: HTTP/1.1 một kết nối xử lý tuần tự (HOL blocking). HTTP/2 chia mỗi request thành stream độc lập, cắt thành frame đan xen trên một kết nối TCP, chạy đồng thời. Go bật HTTP/2 tự động khi dùng TLS.
Đo thật: 20 request trên một kết nối
Gửi 20 request đồng thời (mỗi handler ngủ 50ms), ép cả hai dùng một kết nối để cô lập tác dụng của multiplexing:

Hình 2: 20 request đồng thời trên một kết nối. HTTP/2 xong trong 54 ms (20 stream song song ≈ thời gian một request), HTTP/1.1 mất 1.136 ms (20 × 50ms nối tiếp) — HTTP/2 nhanh hơn 21,2 lần.
- HTTP/2 (một kết nối): 54 ms.
- HTTP/1.1 (một kết nối): 1.136 ms.
Chênh lệch 21,2 lần. Với HTTP/2, 20 stream chạy đồng thời trên một kết nối nên tổng thời gian ≈ thời gian một request (~50ms) cộng chút overhead. Với HTTP/1.1, một kết nối chỉ xử lý một request tại một lần, nên 20 request nối tiếp = 20 × 50ms ≈ 1 giây. Đây là head-of-line blocking đo được bằng con số thật.
Ngoài multiplexing, HTTP/2 còn có: HPACK (nén header, bỏ lặp các header giống nhau giữa các request), flow control per-stream (một stream không chiếm hết băng thông), và server push (ít dùng, đang bị loại bỏ dần).
Đánh đổi cần cân nhắc
Một kết nối lỗi ảnh hưởng mọi stream trên nó. Multiplexing dồn mọi request vào một kết nối TCP — nếu kết nối đó đứt, tất cả stream đang chạy đều hỏng cùng lúc. Với HTTP/1.1 nhiều kết nối, một kết nối đứt chỉ ảnh hưởng request của nó. Trong thực tế HTTP/2 xử lý reconnect tốt, nhưng đây là đánh đổi kiến trúc cần biết.
TCP head-of-line blocking vẫn còn ở tầng dưới. HTTP/2 xóa HOL blocking ở tầng ứng dụng (giữa các stream), nhưng một gói TCP mất vẫn chặn toàn bộ kết nối ở tầng vận chuyển — vì TCP đảm bảo thứ tự byte. Đây là lý do HTTP/3 chuyển sang QUIC (trên UDP): mỗi stream độc lập cả ở tầng vận chuyển, mất gói của stream này không chặn stream khác. HTTP/2 chỉ giải nửa bài toán.
Trong Go, HTTP/2 gần như tự động — nhưng chú ý h2c. net/http tự bật HTTP/2 khi dùng TLS (ALPN thương lượng), nên server HTTPS của bạn đã dùng HTTP/2 mà không cần code thêm. Nhưng HTTP/2 không TLS (h2c) cần cấu hình rõ ràng (h2c.NewHandler), và nhiều client không hỗ trợ h2c — chỉ dùng cho môi trường nội bộ tin cậy. Với gRPC, thư viện lo phần này.
Ba ý mang về
- HTTP/2 multiplexing cho nhiều request chạy song song trên một kết nối TCP: mỗi request là một stream độc lập có id, dữ liệu cắt thành frame đan xen nhau — xóa head-of-line blocking của HTTP/1.1 (một kết nối chỉ một request/lần).
- Chênh lệch đo được rất lớn: đo thật 20 request đồng thời trên một kết nối, HTTP/2 xong 54 ms (song song) so với HTTP/1.1 1.136 ms (nối tiếp) — nhanh hơn 21 lần; cộng thêm HPACK nén header và flow control per-stream.
- Đánh đổi và giới hạn: một kết nối lỗi ảnh hưởng mọi stream, và TCP head-of-line blocking vẫn còn ở tầng vận chuyển (HTTP/3/QUIC mới giải triệt để) — trong Go, HTTP/2 tự bật khi dùng TLS, còn h2c (không TLS) cần cấu hình rõ ràng.
Phần sau ta xét một giao thức thời gian thực khác, giữ kết nối hai chiều mở liên tục: Phần sau dựng một WebSocket server thật trong Go — nâng cấp từ HTTP, đọc/ghi message hai chiều, và so với HTTP/2 stream cùng gRPC bidi.