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ờ

Ảnh chụp đoạn mã Go nền tối minh hoạ TCP vs UDP, cơ chế hai triết lý ngược nhau TCP có kết nối bắt tay số thứ tự cộng ACK cộng truyền lại đảm bảo thứ tự và không mất gói có kiểm soát tắc nghẽn đổi lại nặng hơn có độ trễ chờ ACK truyền lại UDP không kết nối gửi datagram bắn và quên không bắt tay không ACK không đảm bảo tới thứ tự đổi lại nhẹ nhanh độ trễ thấp tự lo tin cậy nếu cần, TCP gửi N gói tuần tự kiểm thứ tự c net Dial tcp addr for i N binary BigEndian PutUint32 b uint32 i c Write b server đọc nếu v khác prev cộng 1 sai thứ tự đếm số gói nhận, UDP bắn dồn dập buffer nhận nhỏ đọc trễ mất gói srv net ListenUDP udp addr srv SetReadBuffer 64 1024 buffer nhận nhỏ c net DialUDP for i N c Write b bắn hết không chờ server đọc sau buffer đã tràn gói bị kernel bỏ mất im lặng localhost gần như không mất phải tạo tình huống này để thấy

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

Ảnh chụp bảng kết quả chạy thật TCP vs UDP output thật, một TCP gửi 200000 gói tuần tự nhận được 200000 trên 200000 đúng thứ tự true mất 0 279 ms TCP chặn người gửi flow control cộng truyền lại không mất đúng thứ tự, hai UDP bắn 200000 datagram dồn dập buffer 64KB đọc trễ nhận được 146 trên 200000 mất 199854 gói 99.9 phần trăm 140 ms UDP không chặn người gửi buffer nhận tràn kernel bỏ gói im lặng UDP nhanh hơn 140 vs 279 ms vì không chờ ACK truyền lại, ba header và mô hình lập trình TCP header 20 byte tối thiểu UDP header 8 byte TCP stream dòng byte liên tục tự nối cắt UDP datagram gói rời rạc giữ nguyên ranh giới mỗi gói, kết luận 99.9 phần trăm mất là tình huống tạo ra bắn dồn đọc trễ buffer nhỏ localhost bình thường gần như không mất gói UDP TCP chọn khi cần đúng và đủ HTTP database truyền file UDP chọn khi cần nhanh nhẹ chấp nhận mất DNS game video QUIC UDP không tệ hơn giao việc đảm bảo tin cậy cho tầng trên tự lo

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ề

  1. 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.
  2. 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).
  3. 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

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ễ.