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:
- Bạn nói: "Tôi sẽ bắt đầu đánh số từ trang 1000." (đề xuất mốc của bạn)
- Đồ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ọ)
- 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ốc | SYN (seq = x) |
| Câu 2 — xác nhận + đề xuất ngược | SYN-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 |
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.
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ờSYNvàseq = 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óACKcho 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ửiSYNcủa riêng nó với ISNy(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 sangESTABLISHED.
Để ý 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.
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:
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.
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 (1855097284 và 882134560) 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
- Khoá Foundations — Điều gì xảy ra khi gõ google.com: handshake này là chặng 2 trong bản đồ 5 chặng — bài này phóng to nó.
- Bài 02 — TCP reliability: sequence number vừa được đồng bộ ở đây sẽ được dùng để đảm bảo dữ liệu tới đủ, đúng thứ tự.
- Bài 04 — TIME_WAIT & hết port: đối xứng với việc mở, kết nối được đóng qua 4 bước — nơi sinh ra
TIME_WAIT. - Bài 06 — Connection pooling & keep-alive: vì handshake tốn 1 RTT, tái dùng kết nối là tối ưu hiệu năng lớn nhất ở tầng này.
9. 📚 Deep Dive — tài liệu gốc
Đọc khi muốn đi tới tận gốc:
- RFC 9293 — Transmission Control Protocol — bản hợp nhất mới nhất của TCP (2022), mục 3.5 mô tả chính xác state machine và 3-way handshake.
- RFC 6528 — Defending Against Sequence Number Attacks — vì sao ISN phải sinh ngẫu nhiên có bí mật.
- RFC 7413 — TCP Fast Open — cách gửi dữ liệu kèm SYN để tiết kiệm 1 RTT.
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:
SYN→SYN-ACK→ACK, 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ẫnSYN(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 điLISTEN → SYN-RCVD → ESTABLISHED. Đọc được trạng thái này trongss/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
Q1Vì sao TCP cần đúng ba bước để mở kết nối, không phải hai?▸
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".Q2Trong 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?▸
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.Q3Vì sao ISN (sequence number khởi đầu) phải ngẫu nhiên thay vì luôn bắt đầu từ 0?▸
Q4Bạ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:Slà 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 đó.
Q5Mộ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?▸
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.Q6Vì 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?▸
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
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