CHƯƠNG 03 · TRANSPORT · ~130 phút

TCP
Handshake & Reliability

TCP là giao thức "đáng tin cậy" — đảm bảo data đến nguyên vẹn, đúng thứ tự, không trùng lặp. Cơ sở của HTTP, SSH, SMTP, mọi connection-oriented protocol. Học chương này, bạn vẽ được 3-way handshake, hiểu sliding window, congestion control, và biết tại sao server bận hay đầy TIME_WAIT.

3.1 TCP là gì?

TCP (Transmission Control Protocol) cung cấp 4 đảm bảo:

  1. Connection-oriented: 2 bên thiết lập connection trước, terminate sau
  2. Reliable: data đến đầy đủ — packet mất → retransmit
  3. In-order: data đến đúng thứ tự — packet đến lộn xộn → reorder
  4. Flow + Congestion control: không "ngập" receiver hay network

Đánh đổi: TCP chậm hơn UDP do overhead. Nhưng đáng tin → dùng cho hầu hết app cần data toàn vẹn (HTTP, SSH, file transfer).

Dùng port để phân biệt app

TCP segment có source portdestination port. OS dùng port để biết giao data cho app nào.

  • Port 0-1023: well-known (cần root): 80=HTTP, 443=HTTPS, 22=SSH, 25=SMTP
  • Port 1024-49151: registered
  • Port 49152-65535: ephemeral — client tự cấp khi connect ra

3.3 3-way Handshake — mở connection

Trước khi gửi data, 2 bên phải "bắt tay". Dùng 3 packet:

Client Server ────── ────── [LISTEN] [CLOSED] ──── 1. SYN ────▶ seq=x (random) [SYN_SENT] [SYN_RCVD] ◀── 2. SYN+ACK ── seq=y (random) ack=x+1 ──── 3. ACK ────▶ ack=y+1 [ESTABLISHED] [ESTABLISHED] ──────── Connection sẵn sàng, exchange data ────────

Tại sao phải 3 packet?

  • Packet 1 (Client→Server): "Tôi muốn connect, đây là seq tôi" — server biết client tồn tại + seq client
  • Packet 2 (Server→Client): ACK packet 1 + gửi seq của server — client biết server alive + seq server
  • Packet 3 (Client→Server): ACK packet 2 — server biết client nhận được seq server

Sau 3 packet: cả 2 bên đều biết seq của nhau và confirm 2 chiều. Đây là yêu cầu tối thiểu cho reliable connection.

📐 Tại sao seq là random?
Để chống "TCP hijacking" — nếu seq luôn 0, attacker đoán được. Random ISN (Initial Sequence Number) giúp khó đoán. Modern OS dùng ISN từ random + hash(src, dst, port).

Hệ quả của 3-way handshake

  • 1.5 RTT để bắt tay xong → trước HTTP/3, mỗi HTTPS connection mất ~3 RTT (TCP + TLS)
  • SYN flood attack: gửi nhiều SYN, server giữ half-open, hết resource
  • Defense: SYN cookies, rate limit

3.4 4-way Termination — đóng connection

Đóng connection cần 4 packet (vì TCP full-duplex, mỗi chiều đóng riêng):

Client Server ────── ────── [ESTABLISHED] [ESTABLISHED] ──── 1. FIN ────▶ "Tôi xong gửi" [FIN_WAIT_1] [CLOSE_WAIT] ◀──── 2. ACK ──── "Đã ghi nhận" [FIN_WAIT_2] Server gửi nốt data ── ── data ── ──▶ ◀──── 3. FIN ──── "Tôi cũng xong" [LAST_ACK] [TIME_WAIT] ──── 4. ACK ────▶ [CLOSED] [Đợi 2*MSL ~ 60s] [CLOSED]

4 packet vì: mỗi bên có FIN + ACK. Nếu đồng thời cả 2 bên FIN, có thể "merge" → 3 packet (rare).

3.5 Sequence Number & ACK

TCP đánh số từng byte trong stream. Seq # của segment = số byte đầu trong segment.

Client gửi 100 byte với seq=1000:
   Segment: seq=1000, data="A...Z..." (100 bytes)

Server nhận, ACK = 1100 ("byte tiếp theo tôi muốn = 1100"):
   Segment: ack=1100

Client gửi tiếp 50 byte:
   Segment: seq=1100, data="X...Y..." (50 bytes)

Server: ack=1150
   ...

Cumulative ACK

ACK=N nghĩa là "đã nhận đủ đến byte N-1, mong byte N tiếp theo". 1 ACK xác nhận tất cả byte trước.

Retransmission

Nếu sender không nhận ACK trong RTO (Retransmission Timeout), gửi lại segment. RTO được tính dựa trên RTT measure.

SACK (Selective Acknowledgment)

Cumulative ACK có vấn đề: nếu mất segment 5 trong 10 segment, sender phải gửi lại 5-10. SACK cho phép báo "tôi nhận 1-4, 6-10, thiếu 5" → chỉ retransmit segment 5.

3.6 Sliding Window — Flow Control

Vấn đề: sender gửi quá nhanh, receiver không kịp xử lý → buffer đầy → drop. Phải có cơ chế "phanh".

Giải pháp: mỗi ACK packet, receiver thông báo Window = "tôi còn chỗ chứa thêm X byte". Sender không gửi quá X byte chưa được ACK.

Sender's view of stream: ┌────────────┬─────────────┬───────────────┐ │ ACKed │ In flight │ Can send │ Can't send (over window) │ (đã ACK) │ (chưa ACK) │ (window còn) │ └────────────┴─────────────┴───────────────┘ ↑ ↑ base base + window Khi nhận ACK, "window" trượt phải.

Window scaling

Field Window 16-bit → max 64KB. Trên link tốc độ cao (10 Gbps), 64KB là quá nhỏ. Option Window Scale (TCP option) nhân thêm 2^N → max ~1GB.

Zero window probe

Nếu receiver advertise window=0 (đầy), sender vẫn định kỳ gửi packet 1-byte để "thăm dò" — nếu không, có thể stuck mãi.

3.7 Congestion Control — đừng làm sập network

Flow control = không "ngập" receiver. Congestion control = không "ngập" network (router buffer, link capacity).

TCP giả định: packet loss = network nghẽn. Khi loss, giảm tốc độ.

Congestion Window (cwnd)

Sender có 2 limit: rwnd (receive window từ receiver) và cwnd (do sender tự điều chỉnh). Số byte in-flight tối đa = min(rwnd, cwnd).

Slow Start

Khi mới connect, không biết network nhanh thế nào. Bắt đầu cwnd = 1 MSS (Maximum Segment Size, ~1460 byte). Mỗi RTT mà không loss, nhân đôi cwnd. "Slow start" — tên gọi sai vì thực ra tăng theo cấp số nhân, nhưng đó là so với "gửi tối đa ngay".

Congestion Avoidance — AIMD

Khi cwnd đạt ngưỡng ssthresh, chuyển sang AIMD (Additive Increase, Multiplicative Decrease):

  • Mỗi RTT không loss: cwnd += 1 MSS (cộng dần)
  • Khi loss: cwnd /= 2 (giảm một nửa) — "back off"
cwnd (kích thước cửa sổ) ▲ │ ╱╲ │ ╱ ╲ loss → giảm 1/2 │ ╱ ╲╱╲ │ ╱ ╲╱╲╲ │ ╱ ╲╲╲ │ slow╱ │start └────────────────────────────▶ time

Các thuật toán congestion control

  • Tahoe (1988): loss → cwnd = 1 (slow start lại từ đầu)
  • Reno (1990): 3 dup ACK = "fast retransmit", cwnd /= 2 (fast recovery)
  • NewReno, SACK
  • Cubic (default Linux): tăng theo cubic function — ổn định trên link tốc độ cao
  • BBR (Google): model bandwidth + RTT thay vì dựa loss
# Linux: xem congestion control hiện dùng
sysctl net.ipv4.tcp_congestion_control
# net.ipv4.tcp_congestion_control = cubic

# Đổi sang BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

3.8 TCP States — bảng trạng thái

StateÝ nghĩa
LISTENServer đang chờ connection
SYN_SENTClient đã gửi SYN, đợi SYN-ACK
SYN_RCVDServer đã nhận SYN, gửi SYN-ACK, đợi ACK cuối
ESTABLISHEDĐang truyền data
FIN_WAIT_1Đã gửi FIN, đợi ACK của bên kia
FIN_WAIT_2Đã nhận ACK, đợi FIN của bên kia
CLOSE_WAITNhận FIN từ bên kia, app phải close
LAST_ACKĐã gửi FIN, đợi ACK cuối
TIME_WAITConnection đã đóng từ phía mình, đợi 2*MSL trước khi cleanup
CLOSEDConnection đã giải phóng
# Quan sát TCP states
ss -tan
# State      Recv-Q Send-Q Local Address:Port  Peer Address:Port
# LISTEN     0      128    0.0.0.0:80          0.0.0.0:*
# ESTABLISHED 0     0      192.168.1.5:54321   142.250.74.14:443
# TIME_WAIT  0      0      192.168.1.5:54322   ...:443

3.9 TIME_WAIT — bug nổi tiếng của server bận

Sau khi đóng connection, bên active close (gửi FIN trước) phải vào TIME_WAIT trong 2*MSL (Maximum Segment Lifetime, thường ~60s).

Tại sao cần TIME_WAIT?

  • Đảm bảo ACK cuối cùng đến được peer: nếu mất, peer retransmit FIN → mình cần còn alive để re-ACK
  • Tránh "ghost packet" của connection cũ đụng connection mới có cùng (src,dst,port,port)

Bug: server reverse proxy / API gateway

nginx/HAProxy là "client" với upstream backend. Mỗi request → tạo TCP connect đến backend. Nếu connect xong xuống → nginx active close → vào TIME_WAIT.

Server bận: 10K req/s × 60s TIME_WAIT = 600K connection trong TIME_WAIT. Mỗi connection chiếm 1 ephemeral port. Linux có ~28K ephemeral port → port exhaustion → kết nối mới fail!

Cách giảm TIME_WAIT

# Reuse TIME_WAIT socket
sysctl -w net.ipv4.tcp_tw_reuse=1

# Tăng range ephemeral port
sysctl -w net.ipv4.ip_local_port_range="10000 65535"

# Tăng số connection backlog
sysctl -w net.core.somaxconn=65535

# Cách tốt nhất: dùng KEEP-ALIVE — tái sử dụng connection
# nginx config:
keepalive_requests 1000;
keepalive_timeout 60s;

Bài học: dùng connection pooling / keep-alive thay vì mở/đóng connection liên tục.

3.10 Nagle's Algorithm & Keep-alive

Nagle's Algorithm

Vấn đề: gõ telnet 1 ký tự = 1 packet với 40 byte header + 1 byte data = 97% overhead. Nagle giải: không gửi nếu còn data chưa được ACK, gom nhiều ký tự thành 1 packet.

Tắt Nagle khi cần latency thấp (real-time game, RPC) bằng TCP_NODELAY socket option.

Keep-alive

Sau 1 thời gian không có data, OS gửi probe packet "Bạn còn alive không?". Nếu không reply → đóng connection (zombie cleanup).

# Linux
sysctl net.ipv4.tcp_keepalive_time      # 7200s default (2 giờ!)
sysctl net.ipv4.tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_probes

HTTP keep-alive (HTTP/1.1 default) khác: là tái sử dụng TCP connection cho nhiều request, tránh tốn 3-way handshake mỗi request.

Bài tập

Bài 1 — Bắt 3-way handshake

Mở Wireshark, filter tcp.flags.syn==1. Browse 1 trang web. Quan sát SYN, SYN-ACK, ACK trên Wireshark.

Bài 2 — TCP states

Chạy server nc -l 8080. Trong terminal khác nc localhost 8080. Mở terminal 3 chạy ss -tan | grep 8080. Quan sát LISTEN, ESTABLISHED. Đóng client (Ctrl+C) → quan sát TIME_WAIT.

Bài 3 — TIME_WAIT count

Trên server bận: ss -tan | grep TIME-WAIT | wc -l. Bao nhiêu? Tại sao cao?

Bài 4 — Vì sao 3-way thay vì 2-way?

Giải thích vì sao 2-way handshake không đủ. Kịch bản nào fail?

Bài 5 — Congestion control demo

Trên Linux: tcpdump -ttt -i any 'host google.com' trong khi curl https://google.com -o /dev/null. Quan sát growth của window/cwnd qua các packet.

Bài 6 — TCP NODELAY

Khi nào nên TCP_NODELAY? Cho 3 use case cụ thể (vd realtime game, API call, telnet).

Bài 7 — Keep-alive trong HTTP

Chạy curl -v https://example.com -v https://example.com. Có thể thấy connection được tái sử dụng không? Thêm --http1.1. Quan sát Connection: header.

🧪 Quiz cuối chương

Câu 1. 3-way handshake gồm các packet?

  • SYN, ACK, ACK
  • SYN, SYN, ACK
  • SYN → SYN-ACK → ACK
  • FIN, FIN-ACK, ACK

Đáp án: SYN → SYN-ACK → ACK. Mất 1.5 RTT. SYN-ACK = 1 packet với cả SYN flag và ACK flag.

Câu 2. Tại sao cần 3 packet, không phải 2?

  • Để cả 2 bên xác nhận seq của nhau (mỗi bên cần ACK seq của bên kia)
  • Vì TCP cần 3 layer
  • Để chống virus
  • Vì IPv4 yêu cầu

Đáp án: confirm 2 chiều. 2-way chỉ confirm 1 chiều — server không biết client nhận được seq mình.

Câu 3. TIME_WAIT tồn tại để?

  • Tăng tốc connection
  • Đảm bảo ACK cuối tới được peer + tránh ghost packet đụng connection mới
  • Chống virus
  • Lưu log

Đáp án: ACK cuối + ghost packet. 2*MSL ~60s — đủ để mọi packet cũ "chết" trong network.

Câu 4. Sliding Window dùng cho?

  • Routing
  • Encryption
  • Flow control — receiver advertise window, sender không gửi quá window byte chưa ACK
  • Compression

Đáp án: flow control. Tránh sender ngập receiver.

Câu 5. AIMD trong congestion control?

  • Additive Increase (mỗi RTT cwnd += 1) + Multiplicative Decrease (loss → cwnd /= 2)
  • Average Increase, Mean Decrease
  • Atomic Increment, Mutex Decrement
  • API Increase Method Decrease

Đáp án: AIMD. Tăng dần, khi mất giảm mạnh (back off). Behavior này tạo nên "saw-tooth" graph.

Câu 6. ACK number = N nghĩa là?

  • Đã nhận N byte data
  • Mong byte tiếp theo có seq = N (đã nhận hết byte trước N)
  • Sequence number kế tiếp tôi sẽ gửi
  • Số connection

Đáp án: byte tiếp theo mong nhận = N. ACK là "cumulative" — xác nhận tất cả byte trước.

Câu 7. Khi server có quá nhiều TIME_WAIT, hệ quả?

  • Server crash
  • RAM hết
  • Ephemeral port exhaustion → connection mới fail (port không còn)
  • Disk đầy

Đáp án: port exhaustion. Linux có ~28K ephemeral port. Server bận với short connections → kẹt. Fix: keep-alive, connection pooling.

Câu 8. TCP_NODELAY tắt cái gì?

  • Tắt encryption
  • Tắt Nagle's algorithm — gửi data ngay, không đợi gom batch
  • Tắt connection
  • Tắt timeout

Đáp án: tắt Nagle. Cho realtime game, RPC — cần latency thấp hơn throughput.

Tổng kết chương 3

  • ✅ TCP = connection-oriented, reliable, in-order, có flow + congestion control
  • 3-way handshake: SYN → SYN-ACK → ACK (1.5 RTT)
  • 4-way termination: FIN, ACK, FIN, ACK
  • ✅ Sequence # đánh số byte; ACK # = byte tiếp theo mong nhận; cumulative
  • Sliding Window = flow control (không ngập receiver)
  • Congestion control: slow start (×2 mỗi RTT) → AIMD (+1 / ÷2)
  • ✅ Linux dùng Cubic mặc định; Google đẩy mạnh BBR
  • TIME_WAIT 2*MSL ~60s — bug port exhaustion ở proxy/gateway bận
  • ✅ Fix: keep-alive, connection pooling, tcp_tw_reuse
  • ✅ Tools: ss -tan, Wireshark, tcpdump
← Chương trước Chương 02: IP, Subnet, Routing Chương kế tiếp Chương 04: UDP →