TCP, HTTP & Web cho Backend/TCP reliability — sequence, ACK, retransmission
3/29
Bài 3 / 29~22 phútTCP & UDP Deep DiveMiễn phí lượt xem

TCP reliability — sequence, ACK, retransmission

IP là best-effort — gói có thể mất, đảo, trùng. Bài này mổ cơ chế TCP đảm bảo dữ liệu tới đủ đúng thứ tự: sequence number đánh số byte, cumulative ACK, RTO vs fast retransmit, SACK, và head-of-line blocking — pitfall nền tảng của HTTP/2.

TL;DR: TCP hứa "dữ liệu tới đủ, đúng thứ tự, không trùng" — nhưng tầng IP bên dưới không hứa gì cả (best-effort). Để giữ lời hứa đó, TCP dùng sequence number đánh số từng byte, cumulative ACK báo "đã nhận đủ tới byte N", và retransmission khi gói mất — kích hoạt bằng timeout (RTO) hoặc 3 duplicate ACK (fast retransmit). SACK mở rộng thêm để tránh truyền lại không cần thiết. Nhưng cơ chế này có cái giá: nếu một gói mất, toàn bộ dữ liệu sau nó bị chặn lại chờ — đây là head-of-line blocking, pitfall lớn nhất của TCP và là lý do HTTP/2 vẫn có giới hạn hiệu năng mà HTTP/3 QUIC giải quyết.

bài 01 bạn vừa thấy handshake đồng bộ sequence number khởi đầu (ISN) cho cả hai chiều. Giờ là lúc dùng sequence number đó: mỗi byte dữ liệu gửi đi đều mang một số thứ tự, và đầu nhận dùng số đó để phát hiện thiếu, ráp lại đúng thứ tự, và xác nhận đã nhận.

Câu hỏi cốt lõi: IP layer là best-effort — nó cố gắng chuyển gói nhưng không cam kết gì. Gói có thể mất (router tràn buffer, link hỏng), đến sai thứ tự (đường đi khác nhau), hoặc trùng (gói cũ kẹt trong mạng rồi tới muộn). TCP phải phát hiện và sửa tất cả những tình huống đó — không cần sự trợ giúp từ tầng dưới.

1. Analogy — Gửi quyển sách theo từng trang riêng lẻ

Hình dung bạn cần gửi một quyển sách 500 trang cho người bạn qua bưu điện, nhưng bưu điện này đặc biệt: mỗi lần chỉ chuyển được 1 trang, có thể làm rơi, có thể giao nhầm thứ tự, và đôi khi giao trùng. Bạn giải quyết thế nào?

Bạn đánh số mỗi trang (trang 1, 2, 3...), và yêu cầu người nhận sau mỗi lô gửi xác nhận "tôi đã có đủ tới trang N". Nếu mất trang 47, người nhận nói "tôi vẫn cần từ trang 47" — bạn chỉ gửi lại từ đó. Nếu trang đến sai thứ tự, người nhận giữ lại và chờ trang thiếu. Trang trùng thì bỏ (số đã có rồi).

Gửi sáchTCP
Đánh số trangSequence number (đánh số từng byte)
"Tôi đã có đủ tới trang N"Cumulative ACK (ack = N+1)
Gửi lại từ trang thiếuRetransmission
Giữ trang đến sớm, chờ trang thiếuReceive buffer — reordering
Trang trùng → bỏDuplicate detection qua seq number
"Tôi đã có trang 47, 49 nhưng thiếu 48"SACK (Selective ACK)
💡 Cách nhớ

Sequence number đánh số byte, không phải gói. Một gói TCP có thể mang 1460 byte — seq number của gói đó là số byte đầu tiên nó mang. ACK trả về là seq number của byte tiếp theo đầu nhận muốn thấy.

2. Sequence number đánh số byte, không phải gói

Sau handshake, cả hai đầu biết ISN của nhau. Mỗi byte dữ liệu gửi đi được gán một sequence number liên tục:

ISN = 1000  (ví du, sau khi handshake)

Goi 1: seq=1001, mang byte 1001-2460  (1460 byte = MSS)
Goi 2: seq=2461, mang byte 2461-3920
Goi 3: seq=3921, mang byte 3921-5380
...

Đầu nhận sau khi nhận gói 1, gửi lại:

ACK: ack=2461  -- "toi nhan du toi byte 2460, muon nhan byte 2461 tiep theo"

Đây là cumulative ACK: một số ACK xác nhận tất cả byte từ ISN tới số đó. Nếu gói 2 đến muộn (mạng định tuyến lại), đầu nhận vẫn giữ nguyên ack=2461 — không tăng lên 3921, vì có "lỗ hổng" ở giữa.

📌 MSS — Maximum Segment Size

MSS (Maximum Segment Size) là lượng dữ liệu tối đa một segment TCP mang, thương lượng trong handshake qua TCP option. Trên Ethernet điển hình MSS = 1460 byte (MTU 1500 byte trừ 20 byte IP header trừ 20 byte TCP header). Xem bài 08 — MTU & fragmentation để hiểu tại sao giới hạn này quan trọng.

3. Mất gói và cơ chế retransmission

Khi một gói mất, đầu nhận không nhận được byte đó nên ACK không tăng lên. Đầu gửi có hai cách phát hiện:

sequenceDiagram
    participant S as Sender
    participant R as Receiver
    S->>R: seq=1001 (byte 1001-2460)
    R-->>S: ack=2461
    S->>R: seq=2461 (byte 2461-3920) -- MAT GOI
    S->>R: seq=3921 (byte 3921-5380)
    S->>R: seq=5381 (byte 5381-6840)
    Note over R: nhan 3921 va 5381<br/>nhung thieu 2461
    R-->>S: ack=2461 (duplicate ACK #1)
    R-->>S: ack=2461 (duplicate ACK #2)
    R-->>S: ack=2461 (duplicate ACK #3)
    Note over S: 3 duplicate ACK -> fast retransmit
    S->>R: seq=2461 (truyen lai)
    R-->>S: ack=6841 (xac nhan ca lo du lieu da buffer)

Cách 1 — RTO (Retransmission Timeout)

Khi gửi một segment, đầu gửi khởi động một bộ đếm RTO (Retransmission Timeout). Nếu hết thời gian mà chưa nhận ACK, segment được truyền lại. RTO không phải số cố định — nó được đo động dựa trên RTT (Round-Trip Time) thực tế của kết nối:

  • SRTT (Smoothed RTT) — RTT trung bình có trọng số (exponential moving average).
  • RTTVAR — variance của RTT.
  • RTO = SRTT + 4 × RTTVAR (công thức Jacobson/Karels, RFC 6298).

Khi RTO xảy ra, đầu gửi truyền lại và tăng gấp đôi RTO (exponential backoff) để tránh spam mạng đang nghẽn.

Cách 2 — Fast retransmit (3 duplicate ACK)

RTO chậm: phải đợi timer hết. Nếu đầu nhận tiếp tục nhận các gói sau gói mất, nó gửi lại ACK cũ (duplicate ACK) mỗi lần — vì ACK cumulative, nó không thể tăng ACK khi còn lỗ hổng.

Khi đầu gửi thấy 3 duplicate ACK liên tiếp, nó kết luận gói đó đã mất và truyền lại ngay lập tức, không đợi RTO. Đây là fast retransmit — nhanh hơn đáng kể vì không tốn cả RTT đợi timer.

⚠️ Vì sao đợi 3 duplicate, không phải 1?

Đầu nhận gửi duplicate ACK ngay khi nhận gói out-of-order — kể cả khi gói chỉ đến sai thứ tự chứ không mất thật. Nếu retransmit ngay sau 1 duplicate ACK, ta sẽ truyền lại nhiều khi mạng chỉ reorder. Thực nghiệm cho thấy 3 là ngưỡng cân bằng: đủ để phân biệt "gói thật sự mất" với "gói đến sai thứ tự nhất thời".

4. SACK — Selective Acknowledgment

Cumulative ACK có một hạn chế: đầu gửi chỉ biết "đầu nhận thiếu từ byte N trở đi", không biết đầu nhận đã có gì ở phía sau lỗ hổng. Kết quả: sau khi truyền lại gói mất, đầu gửi không chắc có cần truyền lại các gói sau đó không.

SACK (Selective ACK, RFC 2018) giải quyết bằng cách cho phép đầu nhận báo cáo chính xác những block byte nào đã nhận được, dù còn lỗ hổng:

ACK: ack=2461, SACK=[3921-6841]
-- "toi muon byte 2461, nhung toi DA CO byte 3921 toi 6841"

Đầu gửi biết chính xác cần truyền lại mỗi mình 2461-3920 — không phải toàn bộ từ 2461 trở đi. SACK là TCP option thương lượng trong handshake; hầu hết OS hiện đại bật mặc định.

# Kiem tra SACK co bat khong (Linux):
sysctl net.ipv4.tcp_sack
# Output: net.ipv4.tcp_sack = 1  (da bat)

5. Head-of-line blocking — khi đảm bảo tin cậy trở thành gánh nặng

Đây là pitfall quan trọng nhất của TCP và là thứ bạn sẽ gặp ngay khi học HTTP/2.

Giả sử bạn tải đồng thời 3 resource qua một kết nối TCP (đây chính xác là cách HTTP/2 multiplexing hoạt động — nhiều stream trong 1 kết nối):

Stream A: byte 1-5000   (CSS file)
Stream B: byte 5001-10000 (JS file)
Stream C: byte 10001-15000 (image)

Nếu gói ở byte 3001-4460 (giữa Stream A) bị mất:

  • Đầu nhận nhận được Stream B và C hoàn chỉnh — nhưng không thể deliver chúng lên application.
  • Tại sao? Vì TCP deliver dữ liệu theo thứ tự byte liên tục. Có lỗ hổng ở byte 3001 → toàn bộ dữ liệu sau đó bị giữ trong receive buffer, chờ retransmit.
  • Stream B và C, dù đã có đủ, cũng bị chặn bởi một gói mất trong Stream A. Đây là head-of-line (HOL) blocking.
HEAD-OF-LINE BLOCKING trong TCP
Stream A: byte 1-3000 OK · 3001-4460 MẤT · 4461-5000 OK
Stream B: byte 5001-10000 ĐÃ CÓ — bị chặn
Stream C: byte 10001-15000 ĐÃ CÓ — bị chặn
TCP buffer chờ retransmit byte 3001-4460 trước khi deliver bất kỳ thứ gì sau đó.

HTTP/2 multiplexing cải thiện nhiều vấn đề của HTTP/1.1 nhưng không giải quyết được HOL blocking ở tầng TCP — vì đó là cơ chế của giao thức vận chuyển, không phải tầng ứng dụng. HTTP/3 giải quyết bằng cách chạy trên QUIC (UDP-based), nơi mỗi stream độc lập và mất gói ở stream này không chặn stream khác. Bạn sẽ gặp lại HOL blocking ở module 2 khi học HTTP/2 và HTTP/3.

6. Pitfall — hiểu nhầm thường gặp

Nhầm 1: "ACK xác nhận từng gói riêng lẻ." ✅ ACK là cumulative — một số ACK xác nhận tất cả byte từ đầu đến số đó. ack=5000 nghĩa là "tôi đã nhận đủ mọi thứ tới byte 4999, muốn byte 5000 tiếp theo". Nếu gói 2 mất nhưng gói 3 và 4 đến, ACK vẫn là số cũ cho tới khi gói 2 được truyền lại và lấp đầy lỗ hổng.

Nhầm 2: "Mất gói là do đường truyền chất lượng kém." ✅ Ở mạng hiện đại, mất gói thường là do router tràn buffer (congestion), không phải lỗi vật lý. Đây là tín hiệu để TCP giảm tốc — và là nền tảng của congestion control mà bài 03 sẽ giải thích.

Nhầm 3: "SACK thay thế hoàn toàn cumulative ACK." ✅ SACK là phần bổ sung, không phải thay thế. Trường ack vẫn dùng cumulative ACK báo "tôi muốn byte N tiếp theo". SACK blocks trong TCP option thêm thông tin về các vùng đã có — giúp đầu gửi tối ưu retransmit, nhưng không thay đổi nghĩa của trường ack.

7. Liên hệ các bài khác

  • Bài 01 — 3-way handshake: handshake đồng bộ ISN cho cả hai chiều — sequence number trong bài này bắt đầu từ đó. HOL blocking là lý do trực tiếp khiến connection pool quan trọng.
  • Bài 03 — Flow & congestion control: sau khi biết TCP retransmit khi mất gói, bài tiếp giải thích TCP phản ứng thế nào để không làm nghẽn mạng — slow start, congestion avoidance, và thuật toán hiện đại BBR.
  • Bài 06 — Connection pooling & keep-alive: HOL blocking là một lý do HTTP/2 dùng nhiều connection TCP song song thay vì một — pool giúp quản lý điều này.
  • Module 2 — HTTP/2 & HTTP/3: HOL blocking ở tầng TCP là chính xác lý do HTTP/3 chuyển sang QUIC (UDP). Bài này đặt nền khái niệm cho bài đó.

8. 📚 Deep Dive — tài liệu gốc

📚 RFC & spec chính thức

Đọc khi muốn đi tới tận gốc:

Ghi chú: RFC 9293 section 3.6 là nơi tra cứu khi tranh cãi về sequence/ACK semantics. RFC 2018 cần thiết khi debug SACK negotiation trong Wireshark (nhìn TCP options trong SYN/SYN-ACK).

9. Tóm tắt

  • Tầng IP là best-effort — gói có thể mất, đảo thứ tự, hoặc trùng. TCP xây đảm bảo tin cậy lên trên bằng cơ chế riêng.
  • Sequence number đánh số từng byte liên tục (không phải gói). Mỗi gói TCP mang seq = số byte đầu tiên nó chứa.
  • Cumulative ACK: ack=N nghĩa là "đã nhận đủ mọi byte trước N, muốn byte N tiếp theo". Có lỗ hổng → ACK không tăng.
  • RTO kích hoạt retransmit sau timeout (tính từ RTT đo được); fast retransmit kích hoạt ngay sau 3 duplicate ACK, nhanh hơn nhiều.
  • SACK (RFC 2018) cho phép báo cáo chính xác các block đã nhận, giúp đầu gửi chỉ truyền lại đúng phần thiếu.
  • Head-of-line blocking: một gói mất chặn toàn bộ dữ liệu sau nó trong receive buffer — kể cả dữ liệu của stream khác đã nhận đủ.
  • HOL blocking là giới hạn cơ bản của TCP; HTTP/3 QUIC giải quyết bằng cách xử lý từng stream độc lập ở tầng transport.

10. Tự kiểm tra

Tự kiểm tra
Q1
Tại sao TCP dùng sequence number đánh số byte, không phải đánh số gói?
kích thước gói thay đổi: một segment có thể mang 1 byte hoặc 1460 byte tuỳ ngữ cảnh (retransmit có thể gộp, nagle algorithm gộp nhỏ...). Đánh số gói sẽ tạo ra không gian địa chỉ không nhất quán — đầu nhận không biết "gói 5" thiếu bao nhiêu byte. Đánh số byte tạo không gian tuyến tính liên tục: mọi byte có đúng một địa chỉ, đầu nhận biết chính xác byte nào thiếu, bao nhiêu byte cần retransmit, và không gian ACK mang đủ thông tin để xác nhận.
Q2
Đầu nhận nhận được gói seq=3921 và seq=5381, nhưng chưa nhận seq=2461. Nó gửi ACK gì?
Nó gửi ack=2461cumulative ACK chỉ tăng khi không có lỗ hổng. Vì byte 2461 chưa đến, ACK không thể vượt qua lỗ hổng đó dù có gói đến sau. Mỗi lần nhận thêm gói out-of-order, đầu nhận lại gửi ack=2461 — đây là các duplicate ACK. Sau 3 duplicate ACK, đầu gửi biết gói mất và kích hoạt fast retransmit.
Q3
Vì sao fast retransmit chờ đúng 3 duplicate ACK, không phải 1 hay 5?
Vì mạng có thể reorder gói mà không thật sự mất. Nếu gói 2 đến sau gói 3 và 4 chỉ vì đường đi khác nhau vài ms, đầu nhận cũng gửi duplicate ACK — nhưng gói không mất. Retransmit ngay sau 1 duplicate ACK sẽ gây truyền lại không cần thiết. Thực nghiệm (RFC 5681) cho thấy mạng hiếm khi reorder quá 2 gói liên tiếp — nên ngưỡng 3 phân biệt được "reorder nhất thời" với "mất thật sự" mà không chờ lâu như RTO.
Q4
Sự khác biệt giữa SACK và cumulative ACK là gì? SACK thay thế hay bổ sung?
Bổ sung, không thay thế. Trường ack vẫn là cumulative — báo byte tiếp theo cần nhận. SACK là TCP option thêm vào header, chứa các block byte đã có nhưng ở phía sau lỗ hổng. Ví dụ: ack=2461, SACK=[3921-6841] nghĩa là "tôi muốn 2461, nhưng tôi ĐÃ CÓ 3921 tới 6841". Nhờ đó đầu gửi chỉ retransmit mỗi block 2461-3920 thay vì gửi lại toàn bộ từ 2461 trở đi.
Q5
Giải thích head-of-line blocking trong TCP. Tại sao nó ảnh hưởng đến HTTP/2?
TCP deliver dữ liệu lên application theo thứ tự byte liên tục. Nếu có lỗ hổng (gói mất), mọi byte sau lỗ hổng bị giữ trong receive buffer dù đã đến đủ — application không nhận được gì cho tới khi retransmit lấp đầy lỗ hổng. HTTP/2 multiplexing nhiều stream trong một kết nối TCP: nếu một gói của stream A mất, stream B và C cũng bị chặn, dù B và C đã có đủ dữ liệu. Đây là HOL blocking ở tầng transport — HTTP/2 không thể giải quyết vì cơ chế nằm ở TCP, không phải HTTP. HTTP/3 dùng QUIC (UDP) với stream-level flow control độc lập để tránh điều này.
Q6
RTO được tính thế nào? Vì sao nó tự điều chỉnh theo kết nối?
RTO tính từ RTT thực tế đo được của từng kết nối: OS liên tục đo SRTT (smoothed RTT — trung bình trọng số) và RTTVAR (variance của RTT), rồi tính RTO = SRTT + 4 × RTTVAR (RFC 6298). Với kết nối nội mạng RTT 1ms, RTO ≈ 4-10ms. Với kết nối xuyên lục địa RTT 150ms, RTO ≈ 600ms-1s. Tự điều chỉnh này tránh hai cực đoan: RTO quá nhỏ gây retransmit sai khi mạng chậm nhất thời; RTO quá lớn làm phục hồi chậm khi gói thật sự mất.
Q7
Khi đầu gửi nhận 3 duplicate ACK và fast retransmit, nó retransmit gói nào — gói đã mất hay toàn bộ từ đó trở đi?
Chỉ gói đã mất. Với cumulative ACK thuần tuý (không SACK), đầu gửi retransmit segment bắt đầu từ giá trị ACK nhận được — tức là từ byte bị thiếu. Nó không retransmit toàn bộ window. Với SACK, đầu gửi còn biết chính xác block nào đầu nhận đã có, nên chỉ truyền lại đúng block thiếu — tránh lãng phí bandwidth truyền lại gói đã đến nơi.

Bài tiếp theo: Flow control & congestion control — vì sao download tăng tốc dần

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Flow control & congestion control — vì sao download tăng tốc dần