TCP, HTTP & Web cho Backend/TCP 3-way handshake — bắt gói thật bằng Wireshark
2/29
Bài 2 / 29~22 phútTCP & UDP Deep DiveMiễn phí lượt xem

TCP 3-way handshake — bắt gói thật bằng Wireshark

Trước khi byte dữ liệu đầu tiên rời máy, ba gói SYN / SYN-ACK / ACK đã bay qua lại. Bài này mổ vì sao cần đúng ba bước, sequence number được đồng bộ thế nào, và cách bắt handshake thật để đọc từng cờ.

TL;DR: Khi bạn gọi fetch("https://api.com"), không một byte HTTP nào được gửi cho tới khi TCP hoàn tất 3-way handshake: client gửi SYN (kèm số thứ tự khởi đầu của nó), server đáp SYN-ACK (xác nhận của client + số thứ tự khởi đầu của server), client gửi ACK. Ba bước này tồn tại để cả hai chiều của kết nối được xác nhận mở và đồng bộ sequence number — nền tảng cho mọi đảm bảo tin cậy sau này. Cái giá là 1 RTT trễ trước byte đầu tiên, lý do trực tiếp khiến keep-alive và connection pool quan trọng. Bài này mổ cơ chế, vẽ state machine, và bắt handshake thật bằng tcpdump/Wireshark.

Bạn vừa biết ở khoá Foundations rằng một request đi qua 5 chặng, và chặng 2 là "TCP bắt tay". Giờ ta phóng to đúng chặng đó.

Câu hỏi mấu chốt: vì sao không gửi thẳng dữ liệu đi? UDP làm thế — gửi-và-quên. Nhưng TCP hứa một điều UDP không hứa: dữ liệu tới đủ, đúng thứ tự, không trùng. Để giữ lời hứa đó, hai đầu phải thống nhất một thứ trước khi có byte nào: chúng sẽ đánh số các byte từ đâu. Cuộc thống nhất đó chính là handshake.

1. Analogy — Hai người chốt "trang số mấy" trước khi đọc chính tả

Hình dung bạn đọc chính tả một văn bản dài cho đồng nghiệp qua điện thoại, đường truyền có lúc rớt tiếng. Để sau này kiểm tra "tôi đã đọc tới đâu", cả hai phải chốt trước bắt đầu đánh số từ đâu:

  1. Bạn nói: "Tôi sẽ bắt đầu đánh số từ trang 1000." (đề xuất mốc của bạn)
  2. Đồng nghiệp đáp: "Nghe rõ trang 1000. Phần tôi đọc lại cho bạn thì bắt đầu từ trang 5000." (xác nhận mốc của bạn + đề xuất mốc của họ)
  3. Bạn chốt: "Nghe rõ trang 5000." (xác nhận mốc của họ)

Sau ba câu, cả hai chiều đều có mốc đánh số chung. Từ giờ mỗi câu đọc ra đều gắn số trang — rớt tiếng thì biết chính xác đọc lại từ trang nào.

Đọc chính tảTCP
Chốt "đánh số từ trang nào"Đồng bộ sequence number khởi đầu (ISN)
Câu 1 — bạn đề xuất mốcSYN (seq = x)
Câu 2 — xác nhận + đề xuất ngượcSYN-ACK (ack = x+1, seq = y)
Câu 3 — bạn xác nhận mốc của họACK (ack = y+1)
"Nghe rõ tới trang N"ACK number — đã nhận đủ tới byte N
💡 Cách nhớ

Handshake không truyền dữ liệu — nó chỉ chốt mốc đánh số cho cả hai chiều. Ba bước là số tối thiểu để mỗi chiều vừa gửi mốc của mình vừa được xác nhận bởi đầu kia.

2. Vì sao phải bắt tay trước khi gửi

TCP là giao thức full-duplex — dữ liệu chạy hai chiều độc lập (client→server và server→client). Để đảm bảo tin cậy, mỗi byte được gán một sequence number (số thứ tự). Đầu nhận dùng số này để: ráp lại đúng thứ tự, phát hiện byte thiếu, và loại byte trùng khi có truyền lại.

Nhưng sequence number bắt đầu từ đâu? Không phải từ 0. Mỗi đầu chọn một ISN (Initial Sequence Number) ngẫu nhiên khi mở kết nối. Handshake là lúc hai đầu trao đổi và xác nhận ISN của nhau. Trước khi việc đó xong, đầu nhận không có cách nào biết byte đầu tiên "đáng lẽ" mang số mấy — nên không thể bắt đầu truyền dữ liệu.

📌 Vì sao ISN ngẫu nhiên, không phải 0

Nếu mọi kết nối đều bắt đầu seq = 0, kẻ tấn công đoán được sequence number sẽ chèn gói giả mạo vào giữa luồng (TCP sequence prediction attack). RFC 6528 yêu cầu ISN sinh từ một hàm băm có thành phần bí mật + đồng hồ, nên gần như không đoán được. Đây là một ví dụ bảo mật nằm ngay trong thiết kế giao thức.

3. Ba bước, đọc từng cờ

Mỗi gói TCP có một tập cờ (flag) trong header. Handshake dùng hai cờ: SYN (synchronize — "đồng bộ sequence number") và ACK (acknowledge — "tôi đã nhận tới đâu").

sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C: CLOSED
    Note over S: LISTEN (dang cho ket noi)
    C->>S: SYN  seq=x
    Note over C: SYN-SENT
    S-->>C: SYN-ACK  seq=y, ack=x+1
    Note over S: SYN-RCVD
    C->>S: ACK  ack=y+1
    Note over C,S: ESTABLISHED — san sang truyen du lieu
  • Bước 1 — SYN (client → server): client chọn ISN của nó là x, gửi gói có cờ SYNseq = x. Nghĩa: "Tôi muốn mở kết nối, và tôi sẽ đánh số byte đầu tiên của tôi từ x."
  • Bước 2 — SYN-ACK (server → client): server làm hai việc trong một gói. Nó ACK cho SYN của client (ack = x + 1 — "tôi đã nhận SYN của bạn, mong byte kế tiếp là x+1"), đồng thời gửi SYN của riêng nó với ISN y (seq = y). Đây là lý do bước 2 gộp cả ACK lẫn SYN — nên handshake chỉ tốn ba gói chứ không phải bốn.
  • Bước 3 — ACK (client → server): client xác nhận ISN của server (ack = y + 1). Sau gói này, cả hai chiều đều có mốc đã được xác nhận → kết nối chuyển sang ESTABLISHED.

Để ý ack = seq + 1: vì sao +1? Cờ SYN được tính như "chiếm một số thứ tự ảo" (dù không mang byte dữ liệu nào), nên đầu kia xác nhận bằng cách trả về số kế tiếp. Quy ước này giúp SYN và FIN (đóng kết nối) cũng được truyền tin cậy như byte thường.

⚠️ Vì sao ba bước, không phải hai

Handshake hai bước (SYN → SYN-ACK) chỉ xác nhận được chiều client→server: server biết client mở, nhưng client chưa xác nhận lại rằng nó đã nhận ISN của server. Bước thứ ba (ACK từ client) đóng vòng cho chiều còn lại. Thiếu nó, một gói SYN cũ kẹt trong mạng rồi tới muộn có thể mở nhầm một kết nối "ma" mà client không hề muốn — đúng kịch bản RFC 9293 dùng để biện minh cho ba bước.

4. State machine — hai phía nhìn khác nhau

Cùng một handshake, nhưng client và server đi qua các trạng thái khác nhau. Biết tên trạng thái giúp bạn đọc output của ss/netstat khi debug:

CLIENT
CLOSED
↓ gửi SYN
SYN-SENT
↓ nhận SYN-ACK, gửi ACK
ESTABLISHED
SERVER
LISTEN
↓ nhận SYN, gửi SYN-ACK
SYN-RCVD
↓ nhận ACK
ESTABLISHED

Server bắt đầu ở LISTEN (đang chờ, do bạn gọi listen() trên socket). Khi nhận SYN, nó tạo một bản ghi kết nối nửa-mở và chuyển sang SYN-RCVD. Chỗ này quan trọng cho bảo mật: các kết nối đang ở SYN-RCVD nằm trong một hàng đợi giới hạn gọi là SYN backlog.

📌 SYN flood — khi handshake bị lợi dụng

Kẻ tấn công gửi ồ ạt gói SYN rồi không bao giờ gửi ACK bước 3. Mỗi SYN chiếm một slot trong SYN backlog của server; backlog đầy thì server từ chối kết nối thật. Đây là SYN flood. Cách phòng chuẩn là SYN cookies: server không lưu trạng thái cho tới khi nhận ACK hợp lệ, mà mã hoá thông tin cần thiết vào chính ISN của nó. Bật bằng sysctl net.ipv4.tcp_syncookies=1 (mặc định đã bật trên Linux hiện đại).

5. Bắt handshake thật

Đủ lý thuyết — soi gói thật. Mở terminal và bắt traffic tới một host bất kỳ bằng tcpdump (lọc đúng port 443):

sudo tcpdump -n -i any 'tcp port 443 and host example.com'
# Trong terminal khac, kich hoat ket noi:
#   curl -sI https://example.com -o /dev/null

Bạn sẽ thấy đúng ba dòng đầu — handshake — trước mọi dữ liệu TLS/HTTP:

IP 10.0.0.5.51324 > 93.184.216.34.443: Flags [S], seq 1855097284
IP 93.184.216.34.443 > 10.0.0.5.51324: Flags [S.], seq 882134560, ack 1855097285
IP 10.0.0.5.51324 > 93.184.216.34.443: Flags [.], ack 882134561

Đọc từng cờ: [S] là SYN, [S.] là SYN-ACK (dấu . là cờ ACK), [.] là ACK đơn thuần. Để ý ack 1855097285 bằng seq 1855097284 cộng 1 — đúng quy ước cộng 1 cho cờ SYN. ISN của hai phía (1855097284882134560) là hai số ngẫu nhiên, không liên quan nhau.

Trong Wireshark, dùng display filter tcp.flags.syn == 1 để chỉ hiện gói SYN, hoặc click chuột phải một gói rồi Follow → TCP Stream để xem cả vòng đời kết nối. Cột "Info" sẽ ghi rõ [SYN], [SYN, ACK], [ACK].

Bạn cũng quan sát trạng thái kết nối phía OS bằng ss:

ss -tan state syn-sent state established | head
# State        Local Address:Port      Peer Address:Port
# ESTAB        10.0.0.5:51324          93.184.216.34:443

6. Cái giá 1 RTT — và vì sao nó định hình kiến trúc

Handshake tốn đúng 1 RTT (một vòng đi-về mạng) trước khi byte HTTP đầu tiên được gửi. Trong nước RTT cỡ 10-30ms; xuyên lục địa có thể 150-300ms. Với HTTPS, sau handshake TCP còn handshake TLS — thêm 1 RTT nữa.

Hệ quả thực tế: nếu app của bạn mở một kết nối mới cho mỗi request, mỗi request gánh 1-2 RTT chỉ để bắt tay, trước cả khi server làm việc. Đây chính là động lực của ba kỹ thuật bạn sẽ gặp ở các bài sau:

  • HTTP keep-alive — tái dùng một kết nối TCP cho nhiều request, trả handshake một lần. Xem bài 06 — Connection pooling & keep-alive.
  • Connection pool — giữ sẵn một rổ kết nối đã ESTABLISHED để mượn ngay.
  • TCP Fast Open (TFO) — cho phép gửi kèm dữ liệu ngay trong gói SYN ở các lần kết nối lại, tiết kiệm 1 RTT (cần OS + server hỗ trợ).
flowchart LR
    A["Moi request<br/>mo ket noi moi"] -->|"1-2 RTT bat tay moi request"| B["Cham"]
    C["Keep-alive<br/>tai dung ket noi"] -->|"bat tay 1 lan"| D["Nhanh"]

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

Nhầm 1: "TCP handshake và TLS handshake là một." ✅ Hai handshake riêng biệt, nối tiếp. TCP handshake (SYN/SYN-ACK/ACK) mở kết nối ở tầng Transport — luôn xảy ra, kể cả với http://. TLS handshake (ClientHello/ServerHello/...) chạy sau đó, bên trong kết nối TCP đã mở, chỉ với https://. Trong tcpdump bạn thấy 3 gói TCP trước, rồi mới tới các gói TLS. Nhầm hai cái khiến bạn debug sai chặng.

Nhầm 2: "Handshake mang dữ liệu HTTP của tôi." ✅ Trừ trường hợp đặc biệt TCP Fast Open, ba gói handshake không mang byte dữ liệu ứng dụng nào — chúng chỉ chốt sequence number. Byte HTTP đầu tiên đi trong gói thứ tư trở đi.

Nhầm 3: "Kết nối kẹt ở SYN-SENT nghĩa là server từ chối." ✅ SYN-SENT kéo dài (client gửi SYN nhưng không nhận SYN-ACK) thường là gói bị nuốt im lặng: firewall DROP (không REJECT), server sai security group, hoặc định tuyến hỏng. Nếu server chủ động từ chối, bạn nhận RST ngay và lỗi là ECONNREFUSED, không phải treo ở SYN-SENT. Phân biệt hai cái thu hẹp nguyên nhân rất nhanh — xem bài 07 — Lỗi mạng thường gặp.

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

9. 📚 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 là nơi tra cứu chuẩn khi tranh cãi "TCP đúng ra phải làm gì". Không cần đọc hết — mục 3.5 (Establishing a Connection) là phần khớp trực tiếp với bài này.

10. Tóm tắt

  • TCP mở kết nối qua 3-way handshake: SYNSYN-ACKACK, tốn đúng 1 RTT trước byte dữ liệu đầu tiên.
  • Mục đích là đồng bộ sequence number (ISN) cho cả hai chiều full-duplex — nền cho mọi đảm bảo tin cậy sau này.
  • Bước 2 gộp cả ACK (cho SYN client) lẫn SYN (của server), nên chỉ cần ba gói chứ không phải bốn.
  • ISN ngẫu nhiên (RFC 6528) để chống tấn công đoán sequence number; cờ SYN "chiếm" một số thứ tự nên được xác nhận bằng ack = seq + 1.
  • Client đi CLOSED → SYN-SENT → ESTABLISHED; server đi LISTEN → SYN-RCVD → ESTABLISHED. Đọc được trạng thái này trong ss/netstat.
  • SYN backlog có giới hạn → SYN flood; phòng bằng SYN cookies.
  • Bắt handshake thật bằng tcpdump/Wireshark: cờ [S], [S.], [.]; chú ý quan hệ ack = seq + 1.
  • 1 RTT mỗi lần bắt tay là lý do keep-alive, connection pool, TFO tồn tại.

11. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao TCP cần đúng ba bước để mở kết nối, không phải hai?
Kết nối TCP là full-duplex — dữ liệu chạy độc lập hai chiều — nên mỗi chiều phải vừa gửi sequence number khởi đầu (ISN) của mình vừa được đầu kia xác nhận. Hai bước (SYN → SYN-ACK) chỉ xác nhận xong chiều client→server; bước thứ ba (ACK từ client) mới xác nhận ISN của server, đóng vòng cho chiều còn lại. Thiếu bước ba, một gói SYN cũ tới muộn có thể mở nhầm kết nối "ma".
Q2
Trong gói SYN-ACK, vì sao server gộp cả ACK lẫn SYN vào một gói thay vì gửi hai gói riêng?
Server cần làm hai việc: xác nhận SYN của client (ack = x+1) và gửi ISN của riêng nó (seq = y với cờ SYN). Cả hai đi cùng hướng server→client tại cùng thời điểm, nên gộp vào một gói tiết kiệm một lần đi mạng. Đây chính là lý do handshake chỉ tốn ba gói chứ không phải bốn.
Q3
Vì sao ISN (sequence number khởi đầu) phải ngẫu nhiên thay vì luôn bắt đầu từ 0?
Nếu ISN đoán được (ví dụ luôn là 0 hoặc tăng đều), kẻ tấn công không nằm trên đường truyền vẫn có thể đoán sequence number và chèn gói giả mạo vào giữa luồng, hoặc giả mạo kết nối. RFC 6528 yêu cầu ISN sinh từ hàm băm có thành phần bí mật cộng đồng hồ, khiến gần như không đoán được — một biện pháp bảo mật nằm ngay trong thiết kế giao thức.
Q4
Bạn thấy ba dòng tcpdump: cờ [S], rồi [S.], rồi [.]. Mỗi cờ là bước nào của handshake?
  • [S] — gói SYN đầu tiên từ client (bước 1).
  • [S.]SYN-ACK từ server: S là cờ SYN, dấu . là cờ ACK (bước 2).
  • [.]ACK đơn thuần từ client (bước 3).

Sau ba gói này kết nối ở trạng thái ESTABLISHED; mọi gói TLS/HTTP đi sau đó.

Q5
Một kết nối kẹt rất lâu ở trạng thái SYN-SENT phía client. Điều này gợi ý nguyên nhân gì, và khác gì với ECONNREFUSED?
Kẹt ở SYN-SENT nghĩa là client đã gửi SYN nhưng không nhận được SYN-ACK — thường do gói bị nuốt im lặng: firewall DROP thay vì REJECT, sai security group, hoặc định tuyến hỏng. Ngược lại, nếu server chủ động từ chối (không có process listen ở port đó), nó trả ngay một gói RST và client báo lỗi ECONNREFUSED tức thì — không treo. Treo lâu nghĩa là nghi bị chặn im lặng; lỗi tức thì nghĩa là nghi không ai listen.
Q6
Vì sao chi phí 1 RTT của handshake lại là lý do chính khiến connection pooling và keep-alive quan trọng?
Mỗi lần mở kết nối mới tốn đúng 1 RTT cho TCP handshake (và thêm 1 RTT nữa cho TLS nếu là HTTPS) trước khi server bắt đầu xử lý. Nếu app mở kết nối mới cho từng request, mỗi request gánh độ trễ bắt tay này. Keep-aliveconnection pool tái dùng các kết nối đã ESTABLISHED, trả chi phí handshake một lần rồi dùng lại nhiều request — thường là tối ưu hiệu năng lớn nhất có được ở tầng vận chuyển.

Bài tiếp theo: TCP reliability — sequence, ACK, retransmission

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

TCP reliability — sequence, ACK, retransmission