Bài trước ta thấy TCP bắt tay ba bước trước khi gửi dữ liệu. Sự kỹ lưỡng đó là để đổi lấy một lời hứa: gói sẽ tới đủ và đúng thứ tự. Nhưng có một giao thức anh em bỏ hết lời hứa ấy để đổi lấy tốc độ và sự nhẹ nhàng: UDP. Chọn sai giữa hai cái này là nguồn của nhiều sự cố — dùng TCP cho video call gây giật lag vì chờ truyền lại, hay dùng UDP cho truyền file làm mất dữ liệu. Bài này (phần 2 loạt Mạng) chạy thật để thấy chính xác hai giao thức khác nhau ở đâu, và vì sao không cái nào "tốt hơn" tuyệt đối.
Cơ chế: hai triết lý ngược nhau
- TCP có kết nối (phải bắt tay), gắn số thứ tự cho từng byte, đòi bên nhận ACK, và truyền lại gói mất. Nó có kiểm soát luồng (flow control) và kiểm soát tắc nghẽn. Kết quả: đảm bảo đủ và đúng thứ tự — nhưng nặng hơn và có độ trễ khi phải chờ ACK/truyền lại.
- UDP không kết nối, gửi từng datagram kiểu "bắn và quên": không bắt tay, không ACK, không truyền lại, không đảm bảo gói tới hay tới đúng thứ tự. Đổi lại: nhẹ, nhanh, độ trễ thấp — và nhường việc đảm bảo tin cậy (nếu cần) cho tầng ứng dụng tự lo.
// TCP: gửi N gói tuần tự, server kiểm thứ tự
c, _ := net.Dial("tcp", addr)
for i := 0; i < N; i++ { binary.BigEndian.PutUint32(b, uint32(i)); c.Write(b) }
// UDP: bắn dồn dập vào buffer nhận nhỏ, đọc trễ -> mất gói
srv, _ := net.ListenUDP("udp", addr)
srv.SetReadBuffer(64 * 1024) // buffer nhận nhỏ
for i := 0; i < N; i++ { c.Write(b) } // bắn hết, KHÔNG chờ

Hình 1: TCP có kết nối + ACK + truyền lại (đảm bảo thứ tự, không mất) so với UDP bắn-và-quên (nhẹ, nhanh, không đảm bảo); demo TCP gửi tuần tự kiểm thứ tự, UDP bắn dồn vào buffer nhỏ để gây mất gói.
Đo thật: 200.000 gói qua mỗi giao thức

Hình 2: Chạy thật — TCP gửi 200.000 gói: nhận đủ 200.000, đúng thứ tự, mất 0, 279ms; UDP bắn dồn dập vào buffer 64KB đọc trễ: chỉ nhận 146, mất 199.854 gói (99,9%), nhưng nhanh hơn (140ms); TCP header 20 byte vs UDP 8 byte.
Đọc kết quả đo được:
- TCP: đủ 100%, đúng thứ tự: gửi 200.000 gói, nhận đủ 200.000, đúng thứ tự, mất 0, trong 279ms. TCP chặn người gửi khi bên nhận chưa kịp đọc (flow control) và truyền lại gói mất — nên không thể mất dữ liệu. Đây là lời hứa của TCP: bạn ghi gì, bên kia đọc đúng thứ đó, đúng thứ tự.
- UDP: mất 99,9% khi bị dồn — và đó không phải lỗi: bắn 200.000 datagram thật nhanh vào một socket có buffer nhận 64KB mà server đọc trễ, chỉ 146 gói sống sót, mất 199.854 (99,9%). Khi buffer nhận đầy, kernel lặng lẽ bỏ các gói tới sau — UDP không có cơ chế chặn người gửi. Nhưng nó nhanh hơn (140ms vs 279ms) chính vì không chờ ACK hay truyền lại.
- Header và mô hình khác nhau: TCP header tối thiểu 20 byte, UDP chỉ 8 byte — UDP nhẹ hơn trên mỗi gói. Về lập trình: TCP là stream (dòng byte liên tục, bạn tự nối/cắt thông điệp), UDP là datagram (mỗi gói rời rạc, giữ nguyên ranh giới) — hai mô hình đòi cách viết code khác nhau.
Đánh đổi cần cân nhắc
99,9% mất là tình huống mình cố tình tạo ra — đừng hiểu nhầm. Trên localhost bình thường, UDP gần như không mất gói. Mình phải dựng đúng kịch bản xấu (bắn dồn dập, buffer nhỏ, đọc trễ) để minh họa sự khác biệt bản chất: khi hệ quá tải, UDP mất gói im lặng còn TCP thì chặn lại và không mất. Con số 99,9% nói lên khả năng mất của UDP dưới áp lực, không phải tỉ lệ mất thường ngày.
UDP không "tệ hơn" — nó chuyển trách nhiệm. Điểm dễ hiểu sai nhất: UDP không phải phiên bản lỗi của TCP. Nó cố tình bỏ đảm bảo tin cậy để tầng trên tự quyết. Với video call, một khung hình mất thì bỏ qua tốt hơn là dừng lại chờ truyền lại (gây giật). Với game, vị trí cũ mất đi thì gói vị trí mới tới ngay quan trọng hơn. QUIC (nền HTTP/3) xây trên UDP rồi tự thêm độ tin cậy có chọn lọc — lấy cái nhẹ của UDP cộng cái đảm bảo tùy biến. Đó là sức mạnh của việc "không đảm bảo sẵn".
Chọn theo hậu quả của việc mất một gói. Quy tắc thực dụng: nếu mất một mẩu dữ liệu là không chấp nhận được (giao dịch, file, trang web) → TCP. Nếu dữ liệu cũ đi rất nhanh và mất một gói không sao (âm thanh/hình ảnh thời gian thực, telemetry, heartbeat, DNS query) → UDP. Đừng chọn theo "cái nào nhanh hơn" chung chung; chọn theo việc bạn có chịu được mất gói hay không.
Ba ý mang về
- TCP đảm bảo đủ và đúng thứ tự, UDP thì không: đo thật TCP nhận 200.000/200.000 đúng thứ tự, mất 0; UDP bị bắn dồn mất 99,9% — vì TCP chặn người gửi + truyền lại, UDP bắn-và-quên để kernel bỏ gói khi buffer đầy.
- UDP đổi độ tin cậy lấy tốc độ và sự nhẹ: đo thật UDP nhanh hơn (140 vs 279ms) và header chỉ 8 byte (vs TCP 20) — nó không tệ hơn, chỉ giao việc đảm bảo tin cậy cho tầng ứng dụng tự lo (như QUIC làm).
- Chọn theo hậu quả của việc mất gói: cần đúng-và-đủ (HTTP, database, file) → TCP; cần nhanh/nhẹ và chịu được mất (DNS, game, video thời gian thực) → UDP; và nhớ 99,9% mất ở đây là kịch bản dựng ra, không phải mức mất thường ngày.
Nguồn
- RFC 768 — User Datagram Protocol: https://www.rfc-editor.org/rfc/rfc768.html
- RFC 9293 — Transmission Control Protocol: https://www.rfc-editor.org/rfc/rfc9293.html
- Cloudflare — What is QUIC? (UDP làm nền cho HTTP/3): https://www.cloudflare.com/learning/performance/what-is-quic/
Phần sau ta soi một chi tiết TCP hay gây trễ bất ngờ: thuật toán Nagle và TCP_NODELAY — vì sao gửi nhiều gói nhỏ có thể bị chậm lại, và khi nào cần tắt Nagle để giảm độ trễ.