Showing posts with label tcp. Show all posts
Showing posts with label tcp. Show all posts

Monday, November 3, 2025

TCP 연결의 핵심 3-Way Handshake의 동작 원리

우리가 인터넷을 통해 웹사이트를 방문하거나, 이메일을 보내거나, 파일을 다운로드할 때마다 보이지 않는 곳에서는 수많은 통신 규약들이 작동하고 있습니다. 그중에서도 데이터 전송의 신뢰성을 보장하는 가장 핵심적인 프로토콜이 바로 TCP(Transmission Control Protocol)입니다. TCP가 어떻게 두 컴퓨터 간에 데이터가 유실되거나 순서가 뒤바뀌지 않도록 보장할 수 있는 것일까요? 그 비밀의 첫 단추는 바로 TCP 3-Way Handshake라고 불리는 연결 수립 과정에 있습니다. 이 과정은 단순히 통신을 시작하겠다는 신호를 보내는 것을 넘어, 양측이 데이터를 주고받을 준비가 되었음을 서로 확인하고, 통신에 필요한 초기 설정값을 동기화하는 매우 정교한 '의식'과도 같습니다.

이 과정을 이해하기 위해 전화 통화에 비유해 보겠습니다. A가 B에게 전화를 겁니다. B가 전화를 받으면 "여보세요?"라고 말하며 자신이 전화를 받았음을 알립니다(첫 번째 단계). A는 B의 목소리를 듣고 "아, B씨 맞으시죠? 저 A입니다. 제 말 들리시나요?"라고 말하며 상대방을 확인하고 자신의 목소리가 들리는지 묻습니다(두 번째 단계). B는 A의 목소리를 듣고 "네, A씨. 잘 들립니다."라고 대답함으로써 양측 모두 서로의 목소리가 들리는 것을 확인하고 비로소 본 통화를 시작할 수 있게 됩니다(세 번째 단계). 만약 이 과정 중 하나라도 빠진다면, 한쪽만 일방적으로 이야기하거나 상대방이 듣고 있는지 확신할 수 없는 불안정한 통화가 될 것입니다. TCP 3-Way Handshake는 이와 같이 컴퓨터 세계에서 신뢰할 수 있는 '통화 연결'을 수립하는 과정입니다.

TCP 3-Way Handshake의 세 단계 심층 분석

TCP 통신은 클라이언트(Client)와 서버(Server) 모델을 기반으로 합니다. 일반적으로 연결을 요청하는 쪽이 클라이언트, 요청을 받아들이는 쪽이 서버가 됩니다. 웹 브라우저가 웹 서버에 접속하는 경우가 대표적인 예입니다. 이 과정은 세 개의 TCP 패킷(Packet)을 교환하는 형태로 이루어집니다. 각 패킷에는 TCP 헤더(Header)라는 중요한 정보가 포함되어 있으며, Handshake 과정에서는 이 헤더의 특정 플래그(Flag) 비트와 순서 번호(Sequence Number)가 핵심적인 역할을 합니다.

주요 플래그는 다음과 같습니다:

  • SYN (Synchronize Sequence Numbers): 연결 요청을 시작할 때 사용됩니다. "지금부터 통신을 시작하고 싶으니, 내 순서 번호를 여기에 맞춰줘"라는 의미를 가집니다.
  • ACK (Acknowledgement): 상대방으로부터 받은 패킷을 잘 받았다는 확인 응답입니다. "네가 보낸 메시지는 여기까지 잘 받았어"라는 의미를 전달합니다.
  • FIN (Finish): 연결을 종료하고자 할 때 사용됩니다.
  • RST (Reset): 비정상적인 연결을 강제로 끊을 때 사용됩니다.

이제 이 플래그들이 실제 Handshake 과정에서 어떻게 사용되는지 단계별로 자세히 살펴보겠습니다.

1단계: 클라이언트의 연결 요청 (SYN)

모든 것은 클라이언트가 서버에게 연결을 요청하는 것으로 시작됩니다. 이때 클라이언트는 SYN 플래그가 1로 설정된 특별한 TCP 패킷을 서버로 전송합니다. 이 패킷은 단순한 요청 신호가 아닙니다. 여기에는 매우 중요한 정보인 초기 순서 번호(Initial Sequence Number, ISN)가 포함되어 있습니다.

순서 번호는 TCP가 데이터를 조각내어 보낼 때, 각 조각(세그먼트)이 올바른 순서로 재조립될 수 있도록 붙이는 고유한 번호입니다. 통신을 시작할 때 양측은 이 순서 번호를 무엇부터 시작할지 서로 알려주어야 합니다. 클라이언트는 이 첫 SYN 패킷에 자신의 ISN을 실어 보냅니다. 예를 들어, 클라이언트가 생성한 ISN이 x라고 가정해 봅시다.

Client -> Server
TCP Packet
Flags: SYN=1, ACK=0
Sequence Number: x

이 패킷을 보낸 클라이언트는 서버로부터 응답이 오기를 기다리는 SYN_SENT 상태로 전환됩니다. 만약 서버가 응답하지 않으면, 클라이언트는 일정 시간 후에 다시 SYN 패킷을 보내는 재전송 메커니즘을 사용합니다.

여기서 중요한 점은 ISN이 예측 가능하지 않은 임의의 숫자로 생성된다는 것입니다. 만약 ISN이 0, 1, 2처럼 순차적이거나 예측 가능하다면, 공격자가 TCP 연결을 가로채는 'TCP 세션 하이재킹(TCP Session Hijacking)' 공격에 취약해질 수 있습니다. 따라서 현대 운영체제는 보안을 위해 매우 복잡한 알고리즘을 통해 무작위적인 ISN을 생성합니다.

2단계: 서버의 응답 및 연결 요청 (SYN-ACK)

서버는 LISTEN 상태에서 클라이언트의 연결 요청을 기다리다가 SYN 패킷을 수신합니다. 패킷을 받으면 서버는 연결을 수락할 준비를 합니다. 이 준비 과정에는 클라이언트와의 통신을 위한 TCB(Transmission Control Block)라는 데이터 구조를 커널 메모리에 할당하는 작업이 포함됩니다. TCB에는 클라이언트의 IP 주소, 포트 번호, 순서 번호 등 연결에 필요한 모든 정보가 저장됩니다.

준비를 마친 서버는 클라이언트에게 응답 패킷을 보냅니다. 이 패킷은 두 가지 중요한 역할을 동시에 수행합니다.

  1. 클라이언트의 요청 수락(ACK): 서버는 클라이언트가 보낸 SYN 패킷을 잘 받았다는 것을 알려주기 위해 ACK 플래그를 1로 설정합니다. 이때 확인 번호(Acknowledgement Number) 필드에는 클라이언트가 보낸 순서 번호(x)에 1을 더한 값(x+1)을 담아 보냅니다. 이는 "나는 당신이 보낸 x번 패킷까지 잘 받았고, 다음에는 x+1번 패킷을 보내주세요"라는 의미입니다.
  2. 서버 자신의 동기화 요청(SYN): 서버 또한 자신의 데이터 전송을 위해 고유한 초기 순서 번호(ISN)를 생성해야 합니다. 서버는 SYN 플래그를 1로 설정하고, 자신의 ISN(예: y)을 순서 번호 필드에 담아 클라이언트에게 보냅니다.

이처럼 두 가지 역할을 한 번에 수행하기 때문에 이 패킷을 SYN-ACK 패킷이라고 부릅니다.

Server -> Client
TCP Packet
Flags: SYN=1, ACK=1
Sequence Number: y
Acknowledgement Number: x + 1

이 패킷을 보낸 후, 서버는 클라이언트의 마지막 확인 응답을 기다리는 SYN_RECEIVED 상태가 됩니다. 이 상태는 잠재적인 보안 위협에 노출될 수 있는 구간이며, 이에 대해서는 나중에 더 자세히 다루겠습니다.

3단계: 클라이언트의 최종 확인 (ACK)

클라이언트는 서버로부터 SYN-ACK 패킷을 받습니다. 이 패킷을 통해 클라이언트는 두 가지 사실을 확인할 수 있습니다. 첫째, 자신이 보낸 연결 요청이 서버에 성공적으로 도달했다는 것(Acknowledgement Number x+1을 통해 확인). 둘째, 서버도 통신을 시작할 준비가 되었으며, 서버의 초기 순서 번호가 y라는 것. 이제 클라이언트는 마지막으로 서버의 연결 요청을 수락했다는 확인 메시지를 보내야 합니다.

클라이언트는 ACK 플래그가 1로 설정된 패킷을 서버에 전송합니다. 이 패킷의 순서 번호는 자신이 이전에 보냈던 번호에 1을 더한 x+1이 됩니다. 그리고 확인 번호 필드에는 서버가 보낸 순서 번호(y)에 1을 더한 값(y+1)을 담아 보냅니다. 이는 "당신이 보낸 y번 패킷까지 잘 받았고, 다음에는 y+1번 패킷을 보내주세요"라는 의미입니다.

Client -> Server
TCP Packet
Flags: SYN=0, ACK=1
Sequence Number: x + 1
Acknowledgement Number: y + 1

이 마지막 ACK 패킷을 보낸 클라이언트는 즉시 ESTABLISHED 상태로 전환되어 서버와 데이터를 주고받을 수 있는 준비를 마칩니다. 서버 역시 이 ACK 패킷을 수신하면 ESTABLISHED 상태로 전환됩니다. 이로써 양측 모두 통신 준비가 완료되었음을 상호 확인했으며, 비로소 신뢰성 있는 양방향 데이터 전송이 시작될 수 있습니다.

전체 과정을 도식화하면 다음과 같습니다.

   Client                                       Server
   (CLOSED)                                     (LISTEN)
      |                                            |
      | -------- SYN (seq=x) -------------------> |
      |                                            |
   (SYN_SENT)                                 (SYN_RECEIVED)
      |                                            |
      | <------- SYN/ACK (seq=y, ack=x+1) -------- |
      |                                            |
      | -------- ACK (seq=x+1, ack=y+1) -------> |
      |                                            |
   (ESTABLISHED)                              (ESTABLISHED)

왜 3-Way Handshake가 필요한가? 2-Way의 한계

여기서 한 가지 근본적인 질문을 던질 수 있습니다. "왜 굳이 세 단계를 거쳐야 할까? 두 단계, 즉 클라이언트가 SYN을 보내고 서버가 ACK으로 응답하면 충분하지 않을까?" 이는 네트워크 프로토콜 설계의 핵심을 꿰뚫는 매우 중요한 질문입니다.

만약 2-Way Handshake (SYN -> ACK) 방식을 사용한다고 가정해 봅시다. 클라이언트가 서버에 SYN 패킷을 보냈는데, 이 패킷이 네트워크 혼잡 등의 이유로 서버에 늦게 도착하는 '지연'이 발생할 수 있습니다. 클라이언트는 응답이 오지 않자 연결 요청이 실패했다고 판단하고, 새로운 SYN 패킷을 다시 보냅니다. 이 두 번째 요청은 정상적으로 처리되어 서버와 연결이 수립되고 데이터 통신 후 정상적으로 종료되었습니다.

그런데 여기서 문제가 발생합니다. 네트워크 어딘가에 떠돌던 첫 번째의 '오래된' SYN 패킷이 뒤늦게 서버에 도착하는 상황입니다. 2-Way Handshake 방식에서는 서버가 SYN 패킷을 받으면 무조건 연결을 수립하고 ACK으로 응답하게 됩니다. 서버는 이것이 오래된 요청인지 알 방법이 없습니다. 따라서 서버는 새로운 연결이 요청된 것으로 착각하고, 해당 연결을 위한 자원(메모리, 소켓 등)을 할당합니다. 하지만 정작 클라이언트는 이미 통신을 마쳤기 때문에 이 연결에 대해 아무것도 모르고, 서버가 보낸 ACK은 무시됩니다. 결국 서버는 사용되지도 않는 연결을 위해 불필요한 자원을 낭비하게 되며, 이런 '반쪽 짜리 연결(Half-Open Connection)'이 많이 쌓이면 서버의 자원이 고갈되어 정상적인 서비스를 제공할 수 없게 됩니다.

3-Way Handshake는 바로 이 문제를 해결합니다. 서버가 SYN-ACK를 보냈을 때, 클라이언트로부터 마지막 ACK 응답이 와야만 연결이 최종적으로 확립됩니다. 만약 오래된 SYN 패킷이 서버에 도착하여 서버가 SYN-ACK를 보내더라도, 현재 클라이언트는 연결을 요청한 적이 없으므로 마지막 ACK를 보내지 않습니다. 서버는 일정 시간 동안 ACK를 기다리다가 응답이 없으면 해당 연결 요청이 비정상적이라고 판단하고 할당했던 자원을 회수합니다. 이처럼 클라이언트의 최종 확인 절차를 통해 '유령' 연결이 생성되는 것을 방지하고, 양측의 연결 의사를 명확하게 확인할 수 있는 것입니다.

3-Way Handshake와 보안: SYN Flooding 공격

3-Way Handshake의 설계는 신뢰성을 확보하는 데 매우 효과적이지만, 역설적으로 이 구조를 악용한 서비스 거부(Denial of Service, DoS) 공격이 가능합니다. 가장 대표적인 공격이 바로 SYN Flooding입니다.

공격 원리는 Handshake의 2단계와 3단계 사이의 허점을 이용하는 것입니다. 서버는 2단계에서 SYN-ACK 패킷을 보낸 후 SYN_RECEIVED 상태로 전환되어 클라이언트의 마지막 ACK를 기다린다고 했습니다. 이때 서버는 해당 연결 요청 정보를 'Backlog Queue'라는 임시 저장 공간에 보관합니다. 만약 악의적인 공격자가 이 Backlog Queue를 가득 채워버리면 어떻게 될까요?

SYN Flooding 공격의 순서는 다음과 같습니다.

  1. 공격자는 출발지 IP 주소를 위조하여 수많은 SYN 패킷을 공격 대상 서버로 전송합니다. 출발지 IP를 존재하지 않는 주소나 공격과 무관한 제3의 주소로 위조하기 때문에 서버가 보내는 SYN-ACK 응답은 절대로 공격자에게 도달하지 않습니다.
  2. 서버는 각 SYN 요청에 대해 SYN-ACK 패킷으로 응답하고, Backlog Queue에 해당 연결 정보를 저장한 뒤 SYN_RECEIVED 상태로 대기합니다.
  3. 하지만 위조된 IP 주소 때문에 서버는 영원히 마지막 ACK 패킷을 받을 수 없습니다.
  4. 공격자가 이 과정을 반복하여 대량의 SYN 패킷을 보내면, 서버의 Backlog Queue는 응답을 기다리는 '반쪽 짜리 연결'들로 가득 차게 됩니다.
  5. 결국 Backlog Queue가 꽉 차면, 서버는 더 이상 새로운 연결 요청을 받아들일 수 없게 됩니다. 이때부터 정상적인 사용자가 서버에 접속을 시도해도 서버가 응답하지 못하는 서비스 거부 상태에 빠지게 됩니다.

이것은 마치 레스토랑에서 허위로 예약 전화만 계속 걸어 모든 테이블을 예약 상태로 만들어 놓고, 정작 실제 손님들은 자리가 없어 들어오지 못하게 만드는 것과 같습니다. 이러한 공격에 대응하기 위해 다양한 방어 기법들이 개발되었습니다.

  • Backlog Queue 크기 늘리기: 가장 단순한 방법이지만, 공격의 규모가 크면 근본적인 해결책이 될 수 없습니다.
  • _
  • SYN_RECEIVED 상태의 Timeout 시간 줄이기: 서버가 ACK를 기다리는 시간을 줄여 Backlog Queue가 더 빨리 비워지도록 하는 방법입니다. 하지만 너무 짧게 설정하면 정상적인 사용자의 연결까지 끊어질 수 있습니다.
  • _
  • SYN Cookies: 가장 효과적인 방어 기법 중 하나입니다. SYN Cookies는 서버가 SYN_RECEIVED 상태에서 연결 정보를 Backlog Queue에 저장하는 대신, 연결에 필요한 중요 정보(클라이언트 IP/Port, 서버 IP/Port 등)를 암호화하여 서버 자신의 ISN(순서 번호)에 숨겨서 클라이언트에게 SYN-ACK로 보내는 방식입니다. 정상적인 클라이언트라면 이 정보를 담은 ACK(Acknowledgement Number: 서버가 보낸 ISN+1)를 다시 보내올 것이고, 서버는 이 ACK 패킷의 확인 번호를 복호화하여 연결 정보를 복원할 수 있습니다. 즉, 마지막 ACK가 도착하기 전까지는 서버가 어떠한 상태 정보도 저장하지 않으므로 Backlog Queue가 고갈될 일이 없습니다.

실제 시스템에서 연결 상태 확인하기

이러한 TCP 연결 상태는 우리 컴퓨터에서도 직접 확인할 수 있습니다. 리눅스, macOS, 윈도우 등 대부분의 운영체제는 netstat이라는 네트워크 상태 확인 명령어를 제공합니다.

터미널이나 명령 프롬프트에서 다음 명령어를 입력하면 현재 시스템의 모든 TCP 연결 상태를 확인할 수 있습니다.


netstat -nat

-n은 주소를 이름 대신 숫자로 표시하는 옵션이고, -a는 모든 소켓을 표시, -t는 TCP 연결만 표시하라는 의미입니다. 이 명령어를 실행하면 다음과 유사한 출력을 볼 수 있습니다.

Proto Local Address Foreign Address State
tcp 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 192.168.1.10:54321 172.217.25.142:443 ESTABLISHED
tcp 192.168.1.10:12345 203.0.113.10:80 SYN_SENT
tcp 127.0.0.1:8080 127.0.0.1:56789 TIME_WAIT

위 표에서 `State` 열을 보면 LISTEN, ESTABLISHED, SYN_SENT 등 우리가 살펴본 TCP 상태들을 직접 눈으로 확인할 수 있습니다. 웹 브라우저로 새로운 사이트에 접속하는 순간 이 명령어를 실행하면, 아주 짧은 순간이지만 `SYN_SENT` 상태의 연결을 포착할 수도 있습니다. 서버 관리자라면 SYN_RECEIVED 상태의 연결이 비정상적으로 많은지 확인함으로써 SYN Flooding 공격의 징후를 파악할 수도 있습니다.

더욱 정밀한 분석을 원한다면 와이어샤크(Wireshark)와 같은 패킷 분석 도구를 사용하여 네트워크를 오가는 실제 SYN, SYN-ACK, ACK 패킷과 그 내용을 직접 들여다볼 수도 있습니다. 이를 통해 각 패킷의 순서 번호와 확인 번호가 어떻게 변화하는지 명확하게 이해할 수 있습니다.

연결의 시작과 끝: 4-Way Handshake와의 비교

연결을 수립하는 과정이 3-Way Handshake라면, 연결을 종료하는 과정은 4-Way Handshake를 통해 이루어집니다. 이 둘을 비교하면 TCP 설계의 정교함을 더욱 깊이 이해할 수 있습니다.

연결 종료 과정이 네 단계인 이유는, 한쪽이 "이제 보낼 데이터가 없다"(FIN)고 선언하더라도, 다른 쪽은 아직 보낼 데이터가 남아있을 수 있기 때문입니다. TCP는 양방향 통신이므로, 각 방향의 데이터 전송이 독립적으로 종료되어야 합니다.

간략한 4-Way Handshake 과정은 다음과 같습니다.

  1. (FIN from Client) 클라이언트가 데이터 전송을 마치고 서버에게 FIN 패킷을 보냅니다. "나는 더 이상 보낼 데이터가 없어."
  2. (ACK from Server) 서버는 클라이언트의 FIN을 잘 받았다는 의미로 ACK 패킷을 보냅니다. 하지만 서버는 아직 클라이언트에게 보낼 데이터가 남아있을 수 있으므로, 바로 연결을 닫지 않고 데이터를 마저 보냅니다.
  3. (FIN from Server) 서버도 모든 데이터 전송을 마치면, 이제 자신도 연결을 종료하겠다는 의미로 FIN 패킷을 클라이언트에게 보냅니다.
  4. (ACK from Client) 클라이언트는 서버의 FIN을 잘 받았다는 마지막 ACK 패킷을 보냅니다. 이 ACK를 받은 서버는 연결을 완전히 닫습니다. 클라이언트는 혹시 모를 네트워크 지연을 고려해 일정 시간(TIME_WAIT 상태)을 기다린 후 연결을 닫습니다.

이처럼 연결 시작 시에는 서버의 응답(ACK)과 동기화 요청(SYN)이 하나의 패킷으로 결합될 수 있지만, 종료 시에는 상대방의 데이터 전송이 끝날 때까지 기다려야 하므로 ACK와 FIN이 별도의 패킷으로 분리되어 네 단계가 되는 것입니다. 이는 데이터의 유실 없는 안전한 종료를 보장하기 위한 TCP의 배려입니다.

결론: 신뢰의 첫걸음

TCP 3-Way Handshake는 단순히 연결을 시작하는 신호 교환을 넘어, 불확실한 네트워크 환경 위에서 신뢰성 있는 통신을 구축하기 위한 정교하고 논리적인 약속입니다. 각 단계는 양측의 통신 의사와 준비 상태를 명확히 확인하고, 데이터 전송에 필수적인 초기 순서 번호를 동기화하며, 잠재적인 오류(오래된 패킷으로 인한 연결)를 방지하는 핵심적인 역할을 수행합니다.

SYN Flooding과 같은 보안 취약점이 존재하기도 하지만, 이는 그만큼 이 과정이 현대 인터넷 통신의 중추적인 역할을 담당하고 있다는 반증이기도 합니다. 우리가 매일같이 사용하는 웹 브라우징, 이메일, 온라인 게임 등 수많은 서비스의 안정성은 바로 이 보이지 않는 세 번의 '악수'로부터 시작됩니다. 3-Way Handshake에 대한 이해는 네트워크의 동작 원리를 파악하고, 문제 발생 시 원인을 진단하는 데 있어 개발자와 엔지니어에게 필수적인 기초 지식이라 할 수 있습니다.

The Unseen Conversation That Powers Your Web Connection

Every time you click a link, load a webpage, or send an email, a silent, lightning-fast conversation takes place before a single byte of your intended data is sent. This isn't just a technical formality; it's a foundational agreement, a digital pact that ensures the information you send and receive arrives intact and in order. This process is the Transmission Control Protocol (TCP) 3-Way Handshake. While it may sound like esoteric jargon, understanding its logic reveals the elegant solution to a fundamental problem of the internet: how to have a reliable conversation over an inherently unreliable network.

Imagine the internet as a massive, chaotic postal system. You can send letters (data packets) to a recipient, but there's no guarantee they will arrive in the order you sent them, or even that they will arrive at all. Some might get lost, others might be duplicated, and some might take a scenic route and arrive late. TCP was designed to solve this. It acts as a meticulous secretary for both the sender and receiver, ensuring every letter is numbered, acknowledged, and re-sent if it goes missing. The 3-way handshake is the initial meeting where these two secretaries agree on the rules of their correspondence, establishing a stable connection before the important messages start to flow.

Why a Handshake is Non-Negotiable for Reliability

Before diving into the mechanics of SYN, SYN-ACK, and ACK, it's crucial to grasp the 'why'. Why can't a client just start sending data to a server? The answer lies in the concept of a "stateful" connection. For a reliable, two-way conversation to occur, both parties must be aware of each other's status and agree to communicate. Both sides need to synchronize their "state" information. This involves several key agreements:

  • Confirmation of Readiness: The client needs to know the server is active, listening, and has the resources to accept a new connection. Simply shouting data into the void is inefficient and prone to failure.
  • Agreement on Starting Points: In TCP, every byte of data is assigned a sequence number. This is how the receiver reassembles packets in the correct order and identifies missing pieces. The handshake is where both the client and server declare their initial sequence number (ISN). This is like two authors agreeing, "I will start numbering my pages at 3,500," and "Excellent, I will start numbering my pages at 8,100." Without this agreement, tracking the conversation would be impossible.
  • Establishment of a Full-Duplex Channel: The handshake confirms that data can flow in both directions simultaneously. The client verifies it can send to the server and receive from the server, and the server does the same for the client.

This initial negotiation prevents chaos. It transforms the unpredictable packet-switching of the internet into a predictable, stream-oriented communication channel, which is what applications like web browsers, email clients, and file transfer programs expect and require to function correctly.

The Three Steps of the Digital Agreement

The handshake involves the exchange of three specific TCP packets. Each packet has its "flags" field in the TCP header set in a particular way to signal its intent. The key flags for the handshake are SYN (Synchronize) and ACK (Acknowledge).

Let's visualize the participants: a Client (e.g., your web browser) wanting to connect to a Server (e.g., a web server hosting a website). Initially, the server is in a `LISTEN` state, passively waiting for connection requests on a specific port (like port 80 for HTTP or 443 for HTTPS). The client is in a `CLOSED` state.

  Client                                     Server
(State: CLOSED)                            (State: LISTEN)
      |                                          |
      |                                          |

Step 1: The Client's Proposal (SYN)

The process begins when the client wants to establish a connection. It crafts a special TCP segment and sends it to the server. This is the first "way" of the handshake.

  • Action: The client sends a TCP packet with the `SYN` flag set to 1.
  • Meaning: This packet essentially says, "I would like to synchronize with you and start a new connection."
  • The Initial Sequence Number (ISN): Critically, this packet also contains a randomly generated Initial Sequence Number, let's call it `Seq=X`. This number isn't zero. It's a 32-bit random number. The randomness is a crucial security feature. If ISNs were predictable, an attacker could more easily forge packets and hijack an ongoing TCP connection. The client is effectively stating, "The first byte of data I will send you will have the sequence number X."
  • State Change: After sending this packet, the client doesn't just wait passively. It changes its state from `CLOSED` to `SYN-SENT`. It is now actively waiting for a specific response from the server and has started a timer. If it doesn't hear back within a certain period, it will assume the packet was lost and re-send it.
  Client                                     Server
(State: SYN-SENT)                          (State: LISTEN)
      |                                          |
      | ------------- SYN (Seq=X) -------------> |
      |                                          |

Think of this as the formal introduction. The client is not just saying "hello," but "Hello, my name is Client, and my first word in this conversation will be numbered X. Are you there and willing to talk?"

Step 2: The Server's Acknowledgment and Counter-Proposal (SYN-ACK)

When the server, listening on the designated port, receives the SYN packet, it understands a client wishes to connect. If the server is able and willing to accept the connection, it crafts a response. This is the second "way" of the handshake.

  • Action: The server sends a TCP packet back to the client with both the `SYN` and `ACK` flags set to 1.
  • Meaning: This packet is doing two jobs at once. The `ACK` part acknowledges the client's request, and the `SYN` part is the server's own synchronization proposal.
  • The Acknowledgment Number: The `ACK` portion is highly specific. The acknowledgment number is set to one more than the client's initial sequence number (`Ack=X+1`). This tells the client, "I have successfully received your proposal that started with number X. The next sequence number I expect from you is X+1." This confirms receipt of the first packet.
  • The Server's Sequence Number: The `SYN` portion of this packet contains the server's own randomly generated Initial Sequence Number, let's call it `Seq=Y`. The server is stating, "I agree to communicate. My own side of the conversation will begin with sequence number Y."
  • State Change: Upon sending the SYN-ACK packet, the server allocates some memory to manage this potential connection and transitions its state from `LISTEN` to `SYN-RECEIVED` (also sometimes referred to as `SYN-RCVD`). Like the client, the server also starts a timer, waiting for the final step of the handshake.
  Client                                     Server
(State: SYN-SENT)                          (State: SYN-RECEIVED)
      |                                          |
      | <--------- SYN/ACK (Seq=Y, Ack=X+1) ----- |
      |                                          |

In our analogy, the server is replying, "It's a pleasure, Client. I acknowledge your starting word will be X. I am ready for your next word, X+1. My own first word will be numbered Y."

Step 3: The Client's Final Confirmation (ACK)

When the client receives the server's SYN-ACK packet, it knows its initial request was successful and that the server is ready. The connection is almost established. The client just needs to send one final confirmation to let the server know it received its proposal. This is the third and final "way" of the handshake.

  • Action: The client sends a TCP packet with the `ACK` flag set to 1.
  • Meaning: This packet confirms receipt of the server's SYN packet.
  • Sequence and Acknowledgment Numbers: The sequence number of this packet is now `Seq=X+1`, as promised. The acknowledgment number is set to one more than the server's initial sequence number (`Ack=Y+1`). This tells the server, "I have successfully received your proposal that started with number Y. The next sequence number I expect from you is Y+1."
  • State Change: After sending this final ACK, the client considers the connection fully established. It transitions its state from `SYN-SENT` to `ESTABLISHED`. At this point, the client can begin sending application data (e.g., an HTTP GET request).

When the server receives this final ACK packet, it checks that the acknowledgment number `Y+1` is correct. If it is, the server also transitions its state from `SYN-RECEIVED` to `ESTABLISHED`. The handshake is complete. A reliable, full-duplex communication channel now exists between the client and the server.

  Client                                     Server
(State: ESTABLISHED)                       (State: ESTABLISHED)
      |                                          |
      | ----------- ACK (Seq=X+1, Ack=Y+1) ----> |
      |                                          |
      | ----------- Application Data... --------> |
      |                                          |
      | <---------- Application Data... -------- |

The client finishes the exchange by saying, "I acknowledge your starting word Y. I am now ready for your next word, Y+1. Let our conversation begin." From this moment on, actual data transfer can commence, with both sides confident that they are synchronized and ready to communicate reliably.

The Handshake in Action: A Deeper Look at the TCP Header

To truly appreciate the process, it helps to understand that these "flags" and "numbers" are just fields within a data structure called the TCP header. A simplified view of the relevant fields in each step looks something like this:

Step Direction Source Port Dest Port Sequence No. Ack No. Flags
1. SYN Client -> Server 54321 (Ephemeral) 443 (Well-known) 1000 (Random X) 0 SYN
2. SYN-ACK Server -> Client 443 54321 8000 (Random Y) 1001 (X+1) SYN, ACK
3. ACK Client -> Server 54321 443 1001 (X+1) 8001 (Y+1) ACK

Notice how the source and destination ports are swapped for the reply, and how the Sequence and Acknowledgment numbers are carefully calculated based on the previous packet. This precise choreography is what makes the connection robust.

When Handshakes Falter: Packet Loss and Retransmission

The internet is not a perfect place. Packets can and do get lost. What happens to the 3-way handshake if one of its constituent packets vanishes into the digital ether?

  • Lost SYN: If the initial SYN from the client is lost, the server will never receive it and thus never reply. The client, having entered the `SYN-SENT` state, has started a timer. When this timer expires (a duration that can vary but is often initially around 1-3 seconds), the client assumes the packet was lost. It doesn't give up. It sends a *new* SYN packet, often doubling the timeout period for the next attempt. It will repeat this several times before finally giving up and reporting a connection error to the application.
  • Lost SYN-ACK: If the server's SYN-ACK response is lost, the client will behave similarly. It's still in the `SYN-SENT` state, waiting. Its timer will expire, and it will re-transmit the original SYN packet. On the server side, which is in the `SYN-RECEIVED` state, receiving another SYN for a connection it's already trying to establish is a clear sign that its SYN-ACK was likely lost. The server will simply re-send the SYN-ACK.
  • Lost Final ACK: This scenario is more interesting. The client sends the final ACK and transitions to `ESTABLISHED`. It now believes it can send data. The server, however, is still in `SYN-RECEIVED` and never receives this ACK. Its own timer will expire, and it will re-transmit the SYN-ACK, assuming *that* was the packet that got lost. When the now-`ESTABLISHED` client receives a duplicate SYN-ACK, it understands what happened. Its own ACK must have been lost. The client's TCP stack is smart enough to know the connection is already established, so it simply discards the duplicate SYN-ACK and sends a new ACK (with the correct sequence numbers) back to the server. Once the server receives this ACK, it finally transitions to `ESTABLISHED`. Meanwhile, any application data the client sent after its first ACK would have been buffered by the server until it entered the `ESTABLISHED` state, ensuring no data is lost.

This built-in resilience, managed by timers and retransmissions, is a core tenet of TCP's reliability. The handshake isn't a fragile, all-or-nothing affair; it's a robust negotiation designed to succeed even in imperfect network conditions.

The Dark Side of the Handshake: The SYN Flood Attack

The very mechanism that makes the handshake reliable also creates a potential vulnerability. Recall that when a server receives a SYN and sends back a SYN-ACK, it enters the `SYN-RECEIVED` state and must allocate a small amount of system resources (memory in a structure called the Transmission Control Block, or TCB) to track this half-open connection. The server expects the final ACK to arrive shortly to complete the connection.

A SYN flood is a type of Denial-of-Service (DoS) attack that exploits this state. An attacker uses a compromised machine or a botnet to send a massive volume of SYN packets to a target server, but with a twist: the source IP addresses in these packets are often spoofed (faked).

The attack unfolds like this:

  1. The attacker sends a flood of SYN packets to the target server, each with a different, random, and unreachable source IP address.
  2. The server dutifully receives each SYN packet. For each one, it allocates TCB resources and sends a SYN-ACK reply back to the spoofed IP address.
  3. Because the source IP addresses are fake, the SYN-ACK packets go nowhere, or to machines that never initiated a connection and will simply discard them.
  4. The server never receives the final ACK for any of these connections. It sits in the `SYN-RECEIVED` state for each one, its TCB table filling up with half-open connections.
  5. Eventually, the server's backlog queue for new connections becomes full. It has no more resources to allocate for new half-open connections. At this point, it can no longer accept *any* new incoming connections, even from legitimate users. The service is effectively denied.
 Attacker                     Victim Server
    | -- SYN (Spoofed IP 1) -->   |
    |                             | --> SYN-ACK (to IP 1, lost)
    | -- SYN (Spoofed IP 2) -->   |
    |                             | --> SYN-ACK (to IP 2, lost)
    | -- SYN (Spoofed IP 3) -->   |
    |                             | --> SYN-ACK (to IP 3, lost)
    | ... (thousands more) ...    | (TCB Table Fills Up)
    |                             |
 Legitimate User                |
    | -- SYN (Real IP) -----> X (Connection Dropped)

Fortunately, several countermeasures have been developed. One of the most effective is the use of SYN cookies. With this technique enabled, when the server receives a SYN, it doesn't immediately allocate a full TCB. Instead, it encodes information about the connection (like the client IP, port, and a server secret) into the sequence number (`Seq=Y`) of the SYN-ACK it sends back. This specially crafted sequence number is the "cookie". The server then discards the initial SYN and forgets everything. It has risked no resources. If the connection is legitimate, the client will reply with an ACK where the acknowledgment number is the cookie plus one. The server can then mathematically validate this cookie from the final ACK. If it's valid, the server knows the client is real and can reconstruct the connection information and move directly to the `ESTABLISHED` state, completely bypassing the `SYN-RECEIVED` state and the need to store half-open connection information.

Saying Goodbye: The Connection Termination (4-Way Handshake)

Just as a connection must be established gracefully, it must also be terminated gracefully. This is accomplished not with a 3-way handshake, but a 4-way handshake, typically involving the `FIN` (Finish) flag.

Why four steps instead of three? Because a TCP connection is full-duplex, each direction of data flow must be shut down independently. One side can indicate it is finished sending data, but it still needs to be able to receive data from the other side. This leads to a "half-closed" state.

The process is as follows (assuming the client initiates the closure):

  1. Client sends FIN: The client application is done sending data. It sends a TCP packet with the `FIN` flag set. This means, "I have no more data to send to you."
  2. Server sends ACK: The server receives the FIN and sends back an ACK to acknowledge it. At this point, the connection becomes half-closed. The server knows the client won't send any more data, but the server itself can continue sending data if it has any left in its buffer.
  3. Server sends FIN: Once the server has also finished sending all its data, it sends its own `FIN` packet to the client. This means, "I am also done sending data."
  4. Client sends ACK: The client receives the server's FIN and replies with a final ACK. After sending this, the client enters a `TIME_WAIT` state to ensure this final ACK is received and to handle any stray, delayed packets. The server receives the ACK and immediately closes the connection. After the `TIME_WAIT` period, the client also closes the connection completely.
  Client                                     Server
(State: ESTABLISHED)                       (State: ESTABLISHED)
      |                                          |
      | ------------- FIN ---------------------> | (1)
      |                                          |
      | <------------- ACK -------------------- | (2)
      | (State: FIN_WAIT_2) (Server may still send data)
      |                                          |
      | <------------- FIN -------------------- | (3)
      |                                          |
      | ------------- ACK ---------------------> | (4)
      |                                          |
(State: TIME_WAIT -> CLOSED)               (State: CLOSED)

This deliberate, four-step process ensures that no data is lost in transit when a connection is closed. Both sides explicitly confirm they are finished sending before the connection is fully torn down.

The Alternative: Why No Handshake is Sometimes Better (TCP vs. UDP)

Understanding TCP's careful handshake brings up an important contrast with its sibling protocol, the User Datagram Protocol (UDP). UDP is the "fire and forget" protocol. It has no handshake. An application using UDP simply starts sending datagrams to a destination. There's no connection to establish, no sequence numbers, no acknowledgments, and no retransmissions.

This sounds unreliable, and it is. UDP offers no guarantee of delivery, order, or duplication protection. So why would anyone use it? The answer is speed. The overhead of establishing and managing a TCP connection, with its handshakes and acknowledgments, introduces latency. For applications where speed is more critical than perfect reliability, UDP is the superior choice. This includes:

  • Live video and audio streaming: If a few video frames or audio packets are lost, it's better to just skip them and show the next available one rather than pausing the entire stream to wait for a retransmission.
  • Online gaming: Real-time game state updates need to arrive as quickly as possible. A delayed packet about a player's position from half a second ago is useless.
  • DNS queries: A DNS request is a small, single query and response. The overhead of a TCP handshake would significantly slow down the process of resolving domain names.

The existence of UDP highlights the truth of the 3-way handshake: it is a deliberate trade-off. TCP sacrifices minimal latency to gain maximum reliability. The handshake is the price of admission for that reliability.

Conclusion: The Foundation of Digital Trust

The TCP 3-way handshake is far more than a simple technical exchange of SYN and ACK packets. It's a fundamental negotiation that underpins the reliability of most of what we do on the internet. It's a process that builds a trusted, stateful communication channel out of the chaotic, stateless fabric of the underlying network. It confirms that both parties are present, ready, and synchronized before the real conversation begins.

Through its careful sequencing, acknowledgments, and built-in timers for retransmission, the handshake ensures that connections are established robustly. And while this process can be exploited, as with SYN flood attacks, the evolution of countermeasures like SYN cookies demonstrates the ongoing effort to harden these foundational protocols. Every time you seamlessly load a webpage, you are witnessing the silent, successful result of this elegant digital agreement, a conversation that has happened billions of times a second, every day, since the dawn of the modern internet.

TCP 3ウェイハンドシェイク 通信の信頼を築く仕組み

私たちが日常的に利用するインターネットの世界では、ブラウザでウェブサイトを開いたり、APIを通じてデータを送受信したりする際に、その裏側で膨大な数の通信が行われています。これらの通信がまるで魔法のように正確かつ確実に目的地に届くのはなぜでしょうか。その答えの核心には、TCP/IPプロトコルスイート、特にTCP(Transmission Control Protocol)が採用している「3ウェイハンドシェイク」という巧妙な仕組みが存在します。多くの開発者がその名前や「SYN, SYN-ACK, ACK」という手順を知識として知ってはいますが、なぜ3回なのか、このやり取りが具体的に何を確立し、アプリケーションの性能やセキュリティにどのような影響を与えるのかという「真実」までを深く理解しているケースは多くありません。本稿では、単なる手順の解説に留まらず、3ウェイハンドシェイクが現代のネットワーク通信の信頼性をいかにして築き上げているのか、その根源的な設計思想からパフォーマンスへの影響、そしてセキュリティ上の意味合いまでを、開発者の視点から深く掘り下げていきます。

第1章 基本の確認:3ウェイハンドシェイクの三段階

まずはじめに、最も基本的なプロセスを再確認しましょう。TCPにおける接続確立のプロセスは、クライアント(接続を開始する側)とサーバー(接続を待ち受ける側)の間で交わされる3つのパケットによって構成されます。この一連のやり取りが「3ウェイハンドシェイク」と呼ばれます。

ステップ1: SYN (Synchronize)

クライアントはサーバーに対して接続を要求するため、最初のパケットを送信します。このパケットには「SYN」フラグが立てられています。これは「Synchronize Sequence Numbers(シーケンス番号を同期したい)」という意思表示です。このとき、クライアントは自身がこれから送信するデータのシーケンス番号の初期値(Initial Sequence Number, ISN)を生成し、パケットに含めてサーバーに通知します。これは、会話を始めるにあたり「私の最初のページ番号はXです」と相手に伝えるようなものです。

クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |

この`seq=x`の `x` は、クライアントがランダムに選んだ32ビットの数値です。このランダム性が、後のセキュリティの議論で重要な役割を果たします。

ステップ2: SYN-ACK (Synchronize-Acknowledge)

SYNパケットを受け取ったサーバーは、クライアントからの接続要求を承諾する意思がある場合、応答パケットを返します。このパケットには「SYN」と「ACK」の両方のフラグが立てられています。

  • SYNフラグ: サーバー自身も、これからクライアントに送信するデータのシーケンス番号の初期値(ISN)を通知する必要があります。これも「私の最初のページ番号はYです」と伝える行為に相当します。
  • ACKフラグ: これはクライアントから受け取ったSYNパケットに対する確認応答です。具体的には、クライアントから送られてきたシーケンス番号 `x` に対して、「あなたのページ番号 `x` を受け取りました。次に私が期待しているのは `x+1` です」という意味で、確認応答番号(Acknowledgement Number)に `x+1` を設定して返します。
クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |
     |   SYN/ACK (seq=y, ack=x+1)               |
     |<-----------------------------------------|
     |                                          |

この時点で、サーバーはクライアントへの接続準備を整え、半開きの接続状態(SYN_RECEIVED状態)になります。

ステップ3: ACK (Acknowledge)

サーバーからSYN-ACKパケットを受け取ったクライアントは、最後の確認応答パケットをサーバーに送信します。このパケットには「ACK」フラグが立てられています。このパケットは、サーバーから送られてきたシーケンス番号 `y` に対して、「あなたのページ番号 `y` を受け取りました。次に私が期待しているのは `y+1` です」と伝えるために、確認応答番号に `y+1` を設定します。

クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |
     |   SYN/ACK (seq=y, ack=x+1)               |
     |<-----------------------------------------|
     |                                          |
     |   ACK (seq=x+1, ack=y+1)                 |
     |----------------------------------------->|
     |                                          |
   [接続確立 (ESTABLISHED)]               [接続確立 (ESTABLISHED)]

この最後のACKパケットをサーバーが受信した時点で、双方向の通信経路が完全に確立され、両者はデータを送受信できる状態(ESTABLISHED状態)になります。これでハンドシェイクは完了です。

第2章 なぜ「3回」なのか?2回ではダメな理由

このプロセスを見て、多くの人が抱く素朴な疑問は「なぜ3回も必要なのか?2回のやり取りでは不十分なのか?」というものです。例えば、クライアントがSYNを送り、サーバーがACKを返す2ウェイハンドシェイクではなぜダメなのでしょうか。この問いに答えることは、TCPが解決しようとしたネットワークの根本的な問題を理解することに繋がります。

ネットワークの不確実性という大前提

IPネットワークは、本質的に「ベストエフォート型」です。つまり、パケットが宛先に届くことを保証しません。パケットは途中で消失したり、複製されたり、到着順序が入れ替わったりする可能性があります。TCPの設計目標は、この信頼性のない基盤の上で、信頼性のある双方向のストリーム通信を実現することでした。

2ウェイハンドシェイクの致命的な欠陥

仮に2ウェイハンドシェイク(クライアントがSYNを送り、サーバーがACKを返すだけ)を想像してみましょう。ここに潜む致命的な問題は、「古い接続要求の亡霊」です。

次のようなシナリオを考えてみてください。

  1. クライアントがサーバーに接続要求(SYN_A)を送ります。
  2. しかし、このSYN_Aはネットワークの遅延により、サーバーにすぐには届きません。
  3. 待ちきれなくなったクライアントは諦めて、もう一度新しい接続要求(SYN_B)を送ります。
  4. SYN_Bは正常にサーバーに届き、サーバーはACKを返し、通信が開始され、やがて正常に終了します。
  5. この通信が終わった後、ネットワークのどこかで迷子になっていた最初のSYN_Aが、遅れてサーバーに届いてしまいます。

もし2ウェイハンドシェイクであれば、サーバーはこの遅れてきたSYN_Aを新しい接続要求だと誤解し、ACKを返して接続を確立してしまいます。しかし、クライアント側はとっくの昔にSYN_Aについては忘れており、新しい接続を確立するつもりはありません。結果として、サーバー側だけが一方的に接続を確立し、リソース(メモリ、ポートなど)を消費し続ける「半開き(Half-Open)」の接続が生まれてしまいます。これは深刻なリソースリークに繋がります。

3ウェイハンドシェイクによる解決策:シーケンス番号の相互確認

3ウェイハンドシェイクは、この問題をシーケンス番号の相互確認によって見事に解決します。重要なのは、接続を確立するために双方が相手のシーケンス番号の初期値を知り、かつ、相手が自分のシーケンス番号の初期値を知ったことを確認するプロセスを踏む点です。

  • ステップ1 (SYN): クライアントは「私のISNは `x` です」と宣言します。
  • ステップ2 (SYN-ACK): サーバーは「あなたのISN `x` を了解しました(ack=x+1)。そして、私のISNは `y` です」と返答します。この時点で、クライアントはサーバーが自分の要求を正しく受信したことを確認できます。また、サーバーのISNも知ることができます。
  • ステップ3 (ACK): クライアントは「あなたのISN `y` を了解しました(ack=y+1)」と返答します。このパケットがサーバーに届くことで、サーバーはクライアントが自分のSYN-ACKを正しく受信したことを確認できます。

この3ステップ目があるおかげで、サーバーはクライアントが「生きている」こと、そして双方向の通信路が確立可能であることを確信できます。先の「古い接続要求の亡霊」シナリオでは、遅れて届いたSYN_Aに対してサーバーがSYN-ACKを返しても、クライアントは既に応答を期待していないため、最後のACKを返すことはありません。サーバーは一定時間待ってもACKが来なければ、この接続要求が不正であると判断し、確保したリソースを解放します。これにより、半開きの接続が放置される事態を防ぐことができるのです。

つまり、3ウェイハンドシェイクは、単に接続を開始する合図ではなく、信頼性のないネットワーク上で、双方がこれから始まる通信の初期状態(シーケンス番号)を完全に同期させるための、必要最小限かつ完璧な手順なのです。

第3章 カーネルの視点:TCPの状態遷移

3ウェイハンドシェイクのプロセスは、オペレーティングシステムのカーネル内で管理されているTCP接続の状態遷移と密接に関連しています。アプリケーション開発者が直接この状態を意識することは少ないかもしれませんが、ネットワークプログラミングやトラブルシューティングを行う上では非常に重要な知識です。

以下に、接続確立時における主要な状態を示します。

      +------------------+
      |      CLOSED      |  <-- 初期状態
      +------------------+
              |
 (app: connect())
              |
      V       |
+---------------------+
|      SYN_SENT     |  <-- クライアント: SYN送信後
+---------------------+
              |               /
 (recv: SYN/ACK)       / (recv: SYN)
 (send: ACK)          / (send: SYN/ACK)
              |      /
      V       V     /
+---------------------+
|   ESTABLISHED     |  <-- 接続確立
+---------------------+


      +------------------+
      |      LISTEN      |  <-- サーバー: 待ち受け状態
      +------------------+
              |
 (recv: SYN)
 (send: SYN/ACK)
              |
      V       |
+---------------------+
|    SYN_RECEIVED   |  <-- サーバー: SYN受信後
+---------------------+
              |
 (recv: ACK)
              |
      V       |
+---------------------+
|   ESTABLISHED     |  <-- 接続確立
+---------------------+

クライアント側の状態遷移

  1. CLOSED: 接続が全く存在しない初期状態です。
  2. SYN_SENT: アプリケーションが `connect()` システムコールなどを呼び出し、カーネルがSYNパケットを送信した後の状態です。この状態でサーバーからのSYN-ACKを待ちます。
  3. ESTABLISHED: サーバーからSYN-ACKを受け取り、最後のACKを送信した後の状態です。この状態になって初めて、アプリケーションは `send()` や `write()` を通じてデータを送信できます。

サーバー側の状態遷移

  1. CLOSED: 初期状態です。
  2. LISTEN: サーバーアプリケーションが `listen()` システムコールを呼び出し、特定のポートでクライアントからの接続を待ち受けている状態です。
  3. SYN_RECEIVED: LISTEN状態のポートでSYNパケットを受け取り、SYN-ACKをクライアントに返信した後の状態です。この状態でクライアントからの最後のACKを待ちます。この状態の接続は、しばしば「ハーフオープン接続」とも呼ばれ、後述するSYNフラッド攻撃の標的となります。
  4. ESTABLISHED: クライアントから最後のACKを受け取った状態です。サーバーは `accept()` システムコールを通じてこの確立された接続をアプリケーションに渡し、データの送受信が開始されます。

これらの状態遷移を理解することで、`netstat` や `ss` といったコマンドの出力が何を意味しているのかを正確に把握し、ネットワークの問題(例えば、`SYN_SENT` のまま接続がタイムアウトする、`SYN_RECEIVED` が大量に滞留しているなど)を診断する手助けとなります。

第4章 ハンドシェイクは交渉の場:TCPオプションの役割

3ウェイハンドシェイクのパケットは、単にSYNやACKのフラグを立ててシーケンス番号を交換するだけではありません。TCPヘッダの「オプション」フィールドを使って、これから始まる通信の様々なパラメータを「交渉」する重要な役割も担っています。これにより、通信経路の特性に合わせてTCPの動作を最適化することができます。

MSS (Maximum Segment Size)

MSSは、1つのTCPセグメントで送信できるペイロード(ユーザーデータ)の最大サイズをバイト単位で指定します。ハンドシェイク中に、クライアントとサーバーはそれぞれ自身のMSSを相手に通知します。最終的に採用されるMSSは、両者が提示した値のうち小さい方になります。

なぜMSSを交渉するのでしょうか?それは、IPレベルでの「フラグメンテーション(断片化)」を避けるためです。データがルーターを通過する際、そのルーターが扱うことができるパケットの最大サイズ(MTU, Maximum Transmission Unit)よりも大きいパケットは、複数の小さなパケットに分割されます。このフラグメンテーションは、ネットワークのオーバーヘッドを増加させ、パケットロスの際の影響を大きくするため、可能な限り避けるべきです。MSSを適切に設定することで(通常は `MSS = MTU - (IPヘッダ長 + TCPヘッダ長)`)、TCPセグメントがIPパケットにカプセル化された際にMTUを超えないようにし、フラグメンテーションを防ぎます。

例えば、一般的なイーサネットのMTUは1500バイトです。この場合、IPヘッダ(20バイト)とTCPヘッダ(20バイト)を引いた1460バイトがMSSの典型的な値となります。

ウィンドウ・スケール・オプション (Window Scale Option)

TCPには、受信側が一度に受信できるデータ量を送信側に伝える「ウィンドウサイズ」という仕組みがあります。これにより、受信側のバッファが溢れるのを防ぐフロー制御を実現しています。しかし、オリジナルのTCPヘッダでは、このウィンドウサイズを表現するフィールドは16ビットしかありませんでした。これは最大で 65,535 バイト (64KB) しか表現できないことを意味します。

現代の高速なネットワーク(ギガビットイーサネットなど)や、遅延が大きいネットワーク(衛星通信など)では、64KBのウィンドウサイズはあまりにも小さすぎます。通信路上に常にデータを流し続けるためには、より大きなウィンドウサイズが必要です。この積はBDP(Bandwidth-Delay Product)と呼ばれ、最適なスループットを出すために必要なバッファサイズを示します。

そこで導入されたのが「ウィンドウ・スケール・オプション」です。ハンドシェイク中にこのオプションを交換することで、双方は16ビットのウィンドウサイズフィールドの値を、指定されたスケールファクタ(2のべき乗)でスケールアップすることに合意します。例えば、スケールファクタが7(2の7乗 = 128)であれば、ヘッダ上のウィンドウサイズが1000でも、実際のウィンドウサイズは 1000 * 128 = 128,000 バイトとして扱われます。これにより、最大で約1ギガバイトまでのウィンドウサイズを表現できるようになり、高速・長距離通信の性能を劇的に向上させることが可能になりました。

SACK Permitted Option (Selective Acknowledgment)

従来のTCPでは、パケットロスが発生すると、失われたパケット以降に受信した全てのパケットを再送する必要がありました(Go-Back-N方式)。これは非常に非効率です。SACKは、受信側がどのセグメントを受け取ったかを不連続なブロックとして送信側に伝えることを可能にする仕組みです。これにより、送信側は失われたセグメントだけを選択的に再送すればよくなり、再送の効率が大幅に向上します。

3ウェイハンドシェイク中に「SACK Permitted」オプションを交換することで、双方はこのSACK機能を使用することに合意します。この合意がなければ、通信中にSACKを用いることはできません。

このように、3ウェイハンドシェイクは、接続の「有無」だけでなく、接続の「質」を決定するための重要な交渉の場でもあるのです。

第5章 パフォーマンスへの影響と最適化

3ウェイハンドシェイクはTCPの信頼性の根幹をなす仕組みですが、その一方でパフォーマンス上のトレードオフも存在します。特に、レイテンシ(遅延)に与える影響は無視できません。

ハンドシェイクがもたらす遅延

ハンドシェイクのプロセスをよく見ると、クライアントが最初のSYNを送信してから、サーバーが最後のACKを受信して接続が完全に確立するまでには、クライアントとサーバー間をパケットが1.5往復する必要があります。しかし、アプリケーションが実際にデータを送信し始めることができるのは、クライアントがSYN-ACKを受信した後です。つまり、アプリケーションデータが流れ始めるまでには、最低でも1 RTT (Round-Trip Time、往復遅延時間) が必要になるのです。

RTTが数ミリ秒程度のLAN環境ではこの遅延は問題になりませんが、モバイルネットワークや国際通信のようにRTTが数百ミリ秒になる環境では、この初期遅延がユーザー体感を大きく損なう原因となります。特に、今日のウェブサイトのように多数の小さなリソース(画像、CSS、JavaScriptファイルなど)を読み込む場合、リソースごとに新しいTCP接続を確立していると、このハンドシェイク遅延が何度も積み重なり、ページの表示が著しく遅くなります。

最適化技術

このハンドシェイクによる遅延を緩和するため、様々な技術が考案されてきました。

HTTP Keep-Alive と HTTP/2

最も基本的な最適化は、一度確立したTCP接続を使い回すことです。HTTP/1.1で導入されたKeep-Alive(持続的接続)は、一つのTCP接続上で複数のHTTPリクエスト/レスポンスをやり取りすることを可能にしました。これにより、ハンドシェイクのコストを最初の一回だけに抑えることができます。さらに進んだHTTP/2では、一つのTCP接続上で複数のリクエスト/レスポンスを並行して多重化できるため、接続を使い回す効率がさらに向上しました。

TCP Fast Open (TFO)

TFOは、ハンドシェイクの遅延そのものを削減しようとする、より野心的な技術です。一度通信したことのあるサーバーに対しては、2回目以降の接続時に、ハンドシェイクの最初のSYNパケットにアプリケーションデータの一部を含めて送信してしまおう、というアイデアです。

  1. 初回接続: 通常の3ウェイハンドシェイクを行います。このとき、サーバーはクライアントに対して「TFOクッキー」と呼ばれる暗号化されたトークンを発行します。
  2. 2回目以降の接続: クライアントは、最初のSYNパケットにこのTFOクッキーとアプリケーションデータ(例えばHTTP GETリクエスト)を一緒に含めて送信します。
  3. サーバー側: サーバーは受け取ったクッキーを検証し、正当であれば、SYN-ACKを返すのと同時に、パケットに含まれていたデータをアプリケーションに渡します。

これにより、クライアントはSYN-ACKを待たずにデータ送信を開始でき、RTTを最大で1回分削減できます。ただし、TFOはリプレイ攻撃などのセキュリティリスクも指摘されており、サーバーとクライアント双方での対応と慎重な設定が必要です。

第6章 セキュリティの側面:SYNフラッド攻撃

3ウェイハンドシェイクの仕組みは、その状態遷移の特性から、古典的かつ強力なDoS(Denial of Service)攻撃である「SYNフラッド攻撃」の標的となってきました。

SYNフラッド攻撃のメカニズム

この攻撃は、サーバーがSYNを受け取ってから最後のACKを受け取るまでの間、`SYN_RECEIVED` という半開きの状態で待機するという性質を悪用します。

  1. 攻撃者は、送信元IPアドレスを偽装した大量のSYNパケットを標的サーバーに送りつけます。
  2. サーバーは、それぞれのSYNパケットに対してSYN-ACKを返信し、`SYN_RECEIVED` 状態の接続情報をメモリ(SYNバックログキュー)に保持して、クライアントからの最後のACKを待ちます。
  3. しかし、SYN-ACKの宛先であるIPアドレスは偽装されている(存在しないか、攻撃とは無関係な第三者のもの)ため、サーバーに最後のACKが返ってくることはありません。
  4. サーバーは、タイムアウトするまで半開きの接続を保持し続けます。攻撃者がSYNパケットを送り続けることで、サーバーのSYNバックログキューはすぐに満杯になってしまいます。
  5. キューが満杯になると、サーバーはそれ以上新たなSYNパケットを受け付けられなくなり、結果として、正規のユーザーからの接続要求をすべて拒否してしまう状態に陥ります。これがサービス不能(Denial of Service)です。

この攻撃が厄介なのは、非常に少ない帯域幅で実行可能であり、かつ送信元IPが偽装されているため攻撃者の特定が困難である点です。

対策技術

SYNフラッド攻撃に対抗するため、いくつかの防御技術が開発されています。

SYNクッキー (SYN Cookies)

SYNクッキーは、SYNフラッド攻撃に対する非常に巧妙な防御策です。この技術を有効にすると、サーバーはSYNパケットを受け取った際に、`SYN_RECEIVED` 状態の情報をメモリに保持しません。その代わり、接続に関する情報(クライアントのIPアドレス、ポート、サーバー自身の秘密鍵など)をハッシュ計算し、その結果をシーケンス番号としてSYN-ACKに含めて返信します。

正規のクライアントであれば、このSYN-ACKに対して正しくACK(シーケンス番号+1)を返してきます。サーバーは、受け取ったACKパケットの確認応答番号から元の情報を逆算・検証することで、このACKが正当なものかを判断できます。検証に成功して初めて、サーバーは接続情報をメモリに確保し、接続を `ESTABLISHED` 状態に移行させます。

これにより、サーバーはACKが来るまで一切のリソースを消費しないため、SYNバックログキューが溢れるという攻撃の根本を無効化できます。

バックログキューの拡大とタイムアウトの短縮

より直接的な対策として、OSのカーネルパラメータを調整し、SYNバックログキューのサイズを大きくしたり、`SYN_RECEIVED` 状態のタイムアウト時間を短くしたりする方法もあります。しかし、これらは攻撃の規模によっては効果が限定的であり、根本的な解決策とはなりにくいです。

第7章 思想の対比:TCPとUDP

3ウェイハンドシェイクの複雑さ、そしてそれがもたらす信頼性とオーバーヘッドを理解するためには、もう一つの主要なトランスポート層プロトコルであるUDP(User Datagram Protocol)と比較するのが最も効果的です。

UDPは、TCPとは対照的に、極めてシンプルなプロトコルです。UDPには接続という概念がなく、ハンドシェイクも行いません。データを送りたいときは、宛先IPアドレスとポート番号を指定して、パケットを「送りっぱなし」にするだけです。そのため、「コネクションレス型」プロトコルと呼ばれます。

この違いがもたらす特性を以下の表にまとめます。

特性 TCP (Transmission Control Protocol) UDP (User Datagram Protocol)
接続形態 コネクション指向(3ウェイハンドシェイクで接続確立) コネクションレス
信頼性 高い(到達確認、順序保証、再送制御あり) 低い(到達も順序も保証しない)
フロー制御 あり(スライディングウィンドウ) なし
輻輳制御 あり(ネットワークの混雑状況に応じて送信量を調整) なし
オーバーヘッド 大きい(ヘッダが20バイト以上、ハンドシェイクの遅延) 小さい(ヘッダが8バイト固定)
主な用途 Web (HTTP/S), メール (SMTP), ファイル転送 (FTP)など、信頼性が重要な通信 DNS, VoIP, オンラインゲーム, ライブストリーミングなど、リアルタイム性が重要な通信

TCPの3ウェイハンドシェイクは、この表で挙げたTCPの持つ高い信頼性、フロー制御、輻輳制御といった高度な機能を実現するための「入場券」のようなものです。この最初の約束事があるからこそ、その後のデータ転送が安定して行えるのです。一方、UDPはそうした約束事を一切省くことで、低遅延と低オーバーヘッドを実現しています。どちらが優れているというわけではなく、アプリケーションの要求に応じて適切なプロトコルを選択することが重要です。この選択の根拠を理解するためにも、TCPがなぜハンドシェイクという手続きを踏むのかを知ることは不可欠です。

結論:単なる手続き以上の意味を持つ約束

TCPの3ウェイハンドシェイクは、単に通信を開始するための形式的な手続きではありません。それは、信頼性のないIPネットワークの上で、確実な双方向通信という城を築き上げるための、最初の、そして最も重要な礎石です。

この3回のやり取りを通じて、クライアントとサーバーは、過去の通信の亡霊に惑わされることなく、お互いの存在を確認し、これからの対話のルール(シーケンス番号)を同期させます。さらに、通信経路に最適化されたパラメータ(MSS、ウィンドウスケールなど)を交渉し、パフォーマンスを最大限に引き出す準備を整えます。この一連のプロセスは、遅延という代償を伴いますが、それと引き換えに得られる「信頼性」は、現代のインターネットアプリケーションのほとんどがその上で成り立っている、かけがえのない価値です。

開発者として、このハンドシェイクの裏側にある設計思想、パフォーマンスへの影響、そしてセキュリティ上の含意を深く理解することは、より堅牢で、高性能で、安全なアプリケーションを構築するための強力な武器となります。次にネットワークの問題に直面したとき、あるいはアプリケーションのパフォーマンスをチューニングしようとするとき、この「通信の約束」がどのように機能しているかを思い出すことで、問題の本質により深く迫ることができるでしょう。

解构TCP三次握手:从连接建立到网络性能优化

在浩瀚的数字世界中,每一次网页浏览、每一次文件下载、每一次在线交流,都离不开一个坚实可靠的基石——传输控制协议(TCP)。它如同网络世界的“邮政系统”,确保我们的数据包能够准确无误、井然有序地送达目的地。而在这个复杂系统中,一切的开端,都源于一个看似简单却蕴含着深刻设计哲学的过程:TCP三次握手(Three-Way Handshake)。

许多开发者和网络爱好者对“SYN、SYN-ACK、ACK”这三个词耳熟能详,但仅仅记住这三个步骤,就像是知道了一场戏剧的开幕、高潮和结尾,却错过了演员们精妙的对话、舞台的布局以及导演的深意。本文将不仅仅是复述这个过程,而是要深入其“灵魂”,从根本上理解为什么是三次而不是两次或四次,它如何巧妙地解决了网络通信中的核心难题,它在操作系统层面如何与资源分配紧密相连,以及它在现代网络攻防和性能优化中扮演着怎样至关重要的角色。这不仅是一次技术的探索,更是一次对网络设计智慧的深度解读。

第一章:对话的开端 - 三次握手的基本流程

在我们深入探讨其背后的“为什么”之前,我们必须首先清晰地掌握“是什么”。TCP三次握手是客户端和服务器之间为了建立一个可靠的TCP连接而进行的信息交换过程。我们可以将其比作一个非常严谨的电话通话过程。

想象一下,你想给朋友打电话确认一件事:

  1. 你(客户端):拿起电话,拨号,然后说:“喂,能听到我吗?”—— 这就是第一次握手。你发出了一个建立连接的请求,并想知道对方是否能收到你的信息。
  2. 朋友(服务器):听到你的声音后,回答说:“能听到,你也能听到我吗?”—— 这是第二次握手。他确认收到了你的请求,并同时反问你是否能收到他的信息。
  3. 你(客户端):听到朋友的回答后,你说:“好的,我也能听到你。那我们开始说正事吧。”—— 这是第三次握手。你确认了你能收到他的信息,至此,双方都确认了双向通信是畅通的。

这个简单的比喻,完美地映射了TCP三次握手的核心逻辑。现在,让我们用技术的语言来精确描述这个过程。

步骤一:客户端发起连接请求(SYN)

当一个应用程序(例如你的浏览器)想要连接到一个服务器(例如 `google.com`)时,客户端的TCP层会创建一个TCP报文段。在这个初始报文段中,一个关键的控制位(flag)——SYN(Synchronize Sequence Numbers)位被设置为1。这标志着这是一个连接请求报文。

  • SYN标志位: `SYN=1`。这个标志位告诉服务器:“我想要和你建立一个连接。”
  • 初始序列号(ISN): 客户端会选择一个随机的初始序列号(Initial Sequence Number),我们称之为 `client_isn` 或 `seq=x`。这个序列号是TCP数据传输的基石,用于确保数据的有序性和可靠性。为什么是随机的?这是一个关键的安全设计,我们将在后续章节详细探讨,它主要是为了防止TCP序列号预测攻击。
  • 客户端状态: 在发送这个SYN包后,客户端的连接状态从 `CLOSED` 变为 `SYN-SENT`。它现在开始静静地等待服务器的响应。

这个过程可以用下面的ASCII艺术图来表示第一步:

  客户端 (Client)                                   服务器 (Server)
  (State: CLOSED)                                  (State: LISTEN)
      |                                                  |
      | ------------ SYN (seq=x) ----------------------> |
      |                                                  |
  (State: SYN-SENT)                                      |

步骤二:服务器响应请求(SYN-ACK)

服务器一直处于 `LISTEN` 状态,监听着特定端口的连接请求。当它收到客户端的SYN报文后,它知道有一个客户端想要建立连接。如果服务器同意建立连接,它会执行以下操作:

  1. 分配资源: 服务器内核会为这个潜在的连接分配TCP缓存和变量,例如创建一个传输控制块(TCB - Transmission Control Block)。这个TCB将用于存储该连接的所有状态信息。
  2. 构建响应报文: 服务器构建一个响应报文,这个报文同时包含了SYN和ACK两个标志位。
    • SYN标志位: `SYN=1`。这部分与客户端的第一步类似,服务器也需要发送自己的同步信号,并选择一个自己的随机初始序列号,我们称之为 `server_isn` 或 `seq=y`。
    • ACK标志位: `ACK=1`。这个标志位表示确认号字段有效。
    • 确认号(Acknowledgment Number): 它的值被设置为客户端的初始序列号加一,即 `ack=x+1`。这是在告诉客户端:“我已经收到了你序列号为x的SYN报文,期待你下一个发送序列号为x+1的数据。”
  3. 服务器状态: 发送完这个SYN-ACK报文后,服务器的连接状态从 `LISTEN` 变为 `SYN-RCVD` (SYN-Received)。它现在等待客户端的最终确认。

此时的通信图如下:

  客户端 (Client)                                   服务器 (Server)
  (State: SYN-SENT)                                (State: LISTEN)
      |                                                  |
      | <----------- SYN-ACK (seq=y, ack=x+1) ---------- |
      |                                                  |
                                                   (State: SYN-RCVD)

步骤三:客户端确认连接(ACK)

客户端收到服务器的SYN-ACK报文后,需要进行检查。它会检查报文中的确认号是否为 `x+1`。如果正确,客户端就知道了从客户端到服务器的路径是通的,并且服务器已经准备好连接。

现在,客户端需要发送最后一个确认报文:

  • ACK标志位: `ACK=1`。
  • 序列号: 此时发送的序列号是 `seq=x+1`,这是对服务器SYN-ACK中确认号的响应。
  • 确认号: 它的值被设置为服务器的初始序列号加一,即 `ack=y+1`。这是告诉服务器:“我已经收到了你序列号为y的SYN报文,期待你下一个发送序列号为y+1的数据。”
  • 客户端状态: 发送这个ACK报文后,客户端的连接状态变为 `ESTABLISHED`。对客户端来说,连接已经建立,它可以开始发送应用层数据了。

当服务器收到这个ACK报文后,它也会检查确认号是否为 `y+1`。如果正确,服务器的连接状态也变为 `ESTABLISHED`。至此,一个全双工的、可靠的TCP连接就完全建立起来了。双方都可以自由地收发数据。

完整的TCP三次握手过程图:

   +----------------+                               +----------------+
   |    客户端      |                               |    服务器      |
   | (Client)       |                               | (Server)       |
   +----------------+                               +----------------+
          |         CLOSED                                    LISTEN          |
          |                                                         |
          |                --- SYN(seq=x, ctl=SYN) --->             |
          |         SYN-SENT                                          |
          |                                                         |
          |                <--- SYN(seq=y, ack=x+1, ctl=SYN,ACK) --- |
          |                                                   SYN-RCVD        |
          |                                                         |
          |                --- ACK(seq=x+1, ack=y+1, ctl=ACK) --->    |
          |         ESTABLISHED                             ESTABLISHED       |
          |                                                         |
          |                <---         数据传输          --->       |
          |                ---         (Data Transfer)       --->       |
          |                <---                               --->       |

握手过程参数总结

为了更清晰地理解每个步骤中TCP头部的关键字段变化,我们可以用一个表格来总结:

步骤 发送方 接收方 SYN标志位 ACK标志位 序列号 (Sequence Number) 确认号 (Acknowledgment Number) 发送方状态变化
1 客户端 服务器 1 0 x (client_isn) 0 (无效) CLOSED -> SYN-SENT
2 服务器 客户端 1 1 y (server_isn) x + 1 LISTEN -> SYN-RCVD
3 客户端 服务器 0 1 x + 1 y + 1 SYN-SENT -> ESTABLISHED

掌握了这三个基本步骤,我们就有了进一步探索的基础。然而,真正的智慧隐藏在这些步骤背后的设计选择中。为什么通信的发起和确认需要如此精确的“三步舞”?这正是我们下一章要深入探讨的核心问题。

第二章:设计的哲学 - 为什么必须是三次握手?

这是TCP面试中最经典的问题,也是理解TCP可靠性设计的关键。要回答这个问题,我们需要用反证法来思考:为什么两次不行?为什么四次没必要?

为什么两次握手不可靠?

让我们设想一个只有两次握手的世界。过程可能是这样的:

  1. 客户端发送SYN请求 (seq=x)。
  2. 服务器收到后,回复一个确认报文(例如ACK,ack=x+1),并认为连接已建立,开始准备接收数据。

这个模型看起来更高效,节省了一次网络往返。但它存在一个致命的缺陷,这个缺陷源于网络中一个不可避免的现实:报文延迟和丢失

考虑以下经典场景,也被称为“已失效的连接请求报文”问题:

  1. 第一次请求: 客户端A发送了一个SYN请求(我们称之为SYN1)给服务器B,请求建立连接。
  2. 网络延迟: 这个SYN1报文因为网络拥堵,在某个路由器中滞留了很长时间,没有立即到达服务器B。
  3. 客户端超时: 客户端A在等待一段时间后,没有收到服务器的确认,认为SYN1丢失了。于是,它重新发送了一个新的SYN请求(我们称之为SYN2)。
  4. 第二次请求成功: 这次SYN2很顺利地到达了服务器B。服务器B回复确认,客户端A也收到了,双方通过SYN2成功建立了连接。数据传输完成后,双方正常关闭了连接。
  5. “幽灵”报文到达: 现在,那个在网络中“迷路”已久的SYN1报文,终于姗姗来迟,到达了服务器B。

现在,问题出现了。在两次握手的模型下,服务器B收到这个“过时”的SYN1后,会误以为这是一个全新的、合法的连接请求。于是,它会向客户端A发送一个确认报文,并单方面地认为连接已经建立,开始为这个“幽灵连接”分配资源(内存、CPU等),然后傻傻地等待客户端A发送数据。然而,客户端A对此一无所知,它早已关闭了之前的连接,根本不会理会这个确认,更不会发送任何数据。

结果是什么?服务器B上出现了一个“半开放连接”(Half-Open Connection)。这个连接会一直占用服务器的资源,直到超时。如果网络中存在大量这种延迟后到达的“幽灵”SYN报文,服务器的资源将被迅速耗尽,导致无法为正常的客户端提供服务,这 фактически 是一种自我攻击。

三次握手如何解决这个问题?

在三次握手的模型中,服务器在收到SYN并回复SYN-ACK后,连接状态是 `SYN-RCVD`,它还没有完全建立连接。它需要等待客户端的第三次握手——那个最终的ACK报文。

回到上面的场景,当那个过时的SYN1到达服务器时:

  1. 服务器回复SYN-ACK。
  2. 服务器进入 `SYN-RCVD` 状态,等待客户端的最终ACK。
  3. 客户端A当前处于 `CLOSED` 状态,它收到了一个不请自来的SYN-ACK。它会检查自己的连接表,发现并没有发起过这样一个连接。因此,客户端内核会判定这是一个无效的报文,并发送一个RST(Reset)报文给服务器。
  4. 服务器收到RST报文后,会立即释放为这个“幽灵连接”分配的资源,关闭这个半开放连接。

即使客户端不发送RST报文(例如,因为防火墙策略),服务器在 `SYN-RCVD` 状态下等待最终ACK也是有超时的。在超时后,它同样会释放资源。因此,第三次握手(客户端的ACK)起到了一个至关重要的最终确认作用。它向服务器证明,客户端确实是“当前”想要建立连接的那个,而不是一个来自过去的“幽灵”。

所以,两次握手的主要问题是:服务器无法确认客户端是否真的收到了自己的同步信号(SYN-ACK),也无法判断收到的SYN是否是过时的。

为什么四次握手没有必要?

既然三次握手如此重要,那么四次会不会更可靠呢?我们可以设想一个四次握手的过程:

  1. 客户端 -> 服务器: SYN
  2. 服务器 -> 客户端: ACK (确认收到了客户端的SYN)
  3. 服务器 -> 客户端: SYN (服务器也发起自己的同步)
  4. 客户端 -> 服务器: ACK (确认收到了服务器的SYN)

仔细观察这个过程,你会发现第二步和第三步——服务器发送ACK和服务器发送SYN——是发往同一个目的地的。从网络效率的角度来看,将这两个报文合并成一个(即SYN-ACK报文)是完全可行的,并且没有任何功能上的损失。TCP协议的设计者们正是这样做的。

将两个步骤合并为一个,既能完成服务器对客户端请求的确认,又能同时发送自己的同步序列号,还减少了一次网络传输的开销。这体现了网络协议设计中的“效率”原则。因此,四次握手虽然在逻辑上可行,但在实践中是冗余和低效的。三次,不多不少,刚刚好。

总结一下,三次握手的核心目的是:

  1. 确认双方的发送能力:客户端发送SYN,服务器能收到,证明客户端的发送能力和服务器的接收能力正常。
  2. 确认双方的接收能力:服务器发送SYN-ACK,客户端能收到,证明服务器的发送能力和客户端的接收能力正常。
  3. 同步初始序列号:双方交换并确认各自的ISN,为后续的可靠数据传输(排序、去重、流量控制)打下基础。
  4. 防止历史连接的干扰:通过第三次握手,确保当前建立的连接是双方都有意愿的,避免了因网络延迟导致的资源浪费。

第三章:操作系统内核的视角 - 状态机与资源管理

TCP协议并非悬浮在空中的理论,它真实地运行在操作系统的内核中。每一次握手,都伴随着内核中连接状态的变迁和系统资源的分配。理解这一点,能让我们从更底层的视角看待TCP连接的生命周期。

TCP连接的状态机

一个TCP连接在其生命周期中,会经历一系列明确定义的状态。这些状态以及它们之间的转换关系,构成了一个有限状态机(Finite State Machine)。三次握手和后续的四次挥手,正是驱动这个状态机运转的关键事件。

让我们聚焦于三次握手相关的状态:

  • CLOSED: 这是连接的初始和最终状态,表示没有任何连接。
  • LISTEN: 这仅存在于服务器端。服务器调用 `listen()` 系统调用后,会创建一个“监听套接字”,进入`LISTEN`状态,等待客户端的连接请求。它像一个开放的港口,等待船只(连接请求)的到来。
  • SYN-SENT: 当客户端调用 `connect()` 系统调用,并发送SYN报文后,客户端套接字进入此状态。它已经发出了邀请,正在等待回音。
  • SYN-RCVD: 当服务器收到SYN报文并发送了SYN-ACK报文后,服务器端的“连接套接字”进入此状态。这是一个中间状态,表示已经收到请求但连接尚未完全建立。我们之前提到的“半开放连接”就处于这个状态。
  • ESTABLISHED: 当客户端发送了第三次握手的ACK,并且服务器也收到了这个ACK后,双方的连接都进入了`ESTABLISHED`状态。这表示连接已成功建立,可以进行双向数据传输。

这个状态转换过程是TCP可靠性的软件实现保障。内核严格按照这个状态机来处理收到的每一个TCP报文段,确保了连接行为的一致性和可预测性。

半连接队列与全连接队列

当服务器处于`LISTEN`状态时,内核会为其维护两个重要的队列:

  1. 半连接队列(Incomplete Connection Queue / SYN Queue):
    • 作用: 用于存放处于 `SYN-RCVD` 状态的连接。当服务器收到客户端的SYN并发出SYN-ACK后,内核会创建一个TCB(传输控制块)的精简结构,并将其放入这个队列中。
    • 大小: 这个队列的大小是有限的,可以通过系统参数(如Linux下的 `net.ipv4.tcp_max_syn_backlog`)进行配置。
    • 风险: 如果这个队列被填满了,服务器将无法处理新的SYN请求,可能会直接丢弃新的SYN报文,或者采取其他策略。这就是后面要讲的SYN Flood攻击的核心目标。
  2. 全连接队列(Completed Connection Queue / Accept Queue):
    • 作用: 用于存放已经完成三次握手,处于 `ESTABLISHED` 状态,但尚未被上层应用程序通过 `accept()` 系统调用取走的连接。
    • 过程: 当服务器收到客户端的第三次握手ACK时,内核会将对应的连接从半连接队列中移除,创建一个完整的TCB,并将其放入全连接队列中,等待应用程序来“认领”。
    • 大小: 这个队列的大小也由 `listen()` 函数的 `backlog` 参数和系统参数(如Linux下的 `net.core.somaxconn`)共同决定。如果这个队列满了,服务器可能也会拒绝新的连接。

理解这两个队列至关重要。它解释了为什么一个高并发的服务器不仅仅需要快速处理业务逻辑,还需要合理配置其网络参数。例如,如果 `accept()` 队列太小,即使服务器硬件性能强大,也可能因为应用程序来不及 `accept()` 连接而导致新的、已经完成握手的连接被内核拒绝,从而影响用户体验。

这两个队列的设计,清晰地将网络协议栈的连接建立过程与上层应用程序的连接处理过程解耦开来,是操作系统实现高并发网络服务的基础架构。

第四章:握手的阴暗面 - 安全漏洞与防御策略

任何一个设计精妙的系统,都可能成为攻击者的目标。TCP三次握手过程,因其需要服务器为尚未完全认证的请求分配资源,天然地存在着被滥用的风险。其中最著名、最古老的攻击之一就是SYN Flood攻击。

SYN Flood攻击:耗尽你的“半连接”资源

SYN Flood(SYN洪水攻击)是一种经典的分布式拒绝服务(DDoS)攻击。其原理简单而粗暴,完美地利用了三次握手中的状态不对称性。

攻击过程:

  1. 伪造源IP: 攻击者控制大量的“僵尸主机”(Botnet),并让它们向目标服务器发送海量的TCP SYN报文。这些SYN报文的源IP地址通常是伪造的、不存在的或不可达的。
  2. 服务器响应: 目标服务器收到这些SYN请求后,会按照正常的TCP流程进行响应。它为每一个SYN请求分配TCB资源,发送SYN-ACK报文,然后将连接放入半连接队列,进入 `SYN-RCVD` 状态。
  3. 等待永不到来的ACK: 由于源IP是伪造的,服务器发出的SYN-ACK报文将永远也到不了真正的“客户端”,因此服务器也永远等不到第三次握手的ACK报文。
  4. 资源耗尽: 大量的连接被堆积在半连接队列中,处于 `SYN-RCVD` 状态。由于半连接队列的长度是有限的,它很快就会被填满。
  5. 拒绝服务: 一旦半连接队列满了,服务器就无法再处理任何新的、合法的SYN请求。所有正常用户都无法与服务器建立TCP连接,从而达到了拒绝服务的目的。

这种攻击的巧妙之处在于,它利用了非常小的攻击流量(只需要发送不大的SYN包)就能撬动服务器分配远大于其请求的资源(TCB、内存等),造成了极高的攻击放大效应。

防御之道:SYN Cookies的智慧

如何应对SYN Flood攻击?直接增加半连接队列的大小是一种简单的方法,但这治标不治本,攻击者只需增加攻击流量即可再次将其填满。更根本的解决方案是,在确认客户端身份真实有效之前,不分配任何资源。SYN Cookies技术正是基于这一思想的绝妙发明。

SYN Cookies由著名密码学家Daniel J. Bernstein提出,其核心思想是:当收到SYN报文时,服务器不立即分配TCB,而是将连接的关键信息通过一种特殊算法编码后,作为初始序列号(ISN)放在SYN-ACK报文中发回给客户端。

SYN Cookies工作流程:

  1. 收到SYN: 当服务器收到一个SYN请求时,如果半连接队列已满,它会触发SYN Cookies机制。
  2. 生成Cookie: 服务器不创建TCB,而是根据以下信息生成一个“Cookie”(实际上就是一个特殊的序列号 `y`):
    • 源IP、源端口,目标IP、目标端口
    • 一个随时间缓慢变化的服务器内部密钥(secret)
    • 其他一些TCP选项信息(如MSS)
    这个生成过程通常使用一个快速的哈希函数(例如MD5或SHA-1的一部分)。
  3. 发送SYN-ACK: 服务器将这个生成的Cookie作为自己的ISN(`seq=y`)发送SYN-ACK报文给客户端。发送完毕后,服务器立即丢弃关于这个SYN请求的任何信息,不保留任何状态。
  4. 客户端响应:
    • 合法客户端: 如果客户端是合法的,它会收到SYN-ACK,并回复一个ACK报文,其确认号为 `ack=y+1`。
    • 攻击者: 如果是攻击者(伪造的IP),它收不到SYN-ACK,自然也不会有任何回应。
  5. 服务器验证Cookie: 服务器收到最终的ACK报文后,它看到确认号是 `y+1`。服务器并不记得自己发送过序列号为 `y` 的报文。但它会执行一个逆向计算:
    1. 从 `y+1` 中减去1,得到Cookie `y`。
    2. 用与生成时相同的密钥和ACK报文中的客户端IP、端口等信息,重新计算一次Cookie。
    3. 如果重新计算出的Cookie与收到的 `y` 一致,则证明这个ACK是对刚才发送的SYN-ACK的有效响应。
  6. 建立连接: 验证通过后,服务器才真正地分配TCB,并直接将连接置于 `ESTABLISHED` 状态,绕过了 `SYN-RCVD` 状态和半连接队列。

SYN Cookies的智慧在于,它将本应由服务器存储的半连接状态信息,“外包”给了客户端。通过让客户端在第三次握手的ACK中“带回”这些信息,服务器实现了无状态的连接验证。这使得服务器在面对SYN洪水时,能够保持极强的韧性,因为攻击性的SYN包不会消耗服务器任何内存资源。

当然,SYN Cookies也有一些小的缺点,例如它无法携带所有的TCP选项信息,因为它需要利用序列号字段来编码。但在大多数情况下,这种为应对攻击而做的取舍是完全值得的。

第五章:速度与激情 - 握手过程的性能影响与优化

TCP的设计将可靠性放在了首位,而三次握手是这种可靠性的基石。然而,这种可靠性是有代价的,最直接的代价就是延迟

完成一次完整的三次握手,需要一个完整的网络往返时间(Round-Trip Time, RTT)。也就是说,在客户端发送第一个数据包之前,必须等待至少一个RTT的时间。对于高延迟的网络(例如卫星通信或跨洋连接),这个延迟可能高达数百毫秒。对于许多对延迟敏感的应用(如网页加载、API调用、在线游戏),这个初始延迟是不可忽视的性能瓶颈。

特别是对于大量短连接的应用(例如早期的HTTP/1.0),每个请求都需要建立一个新的TCP连接。这意味着每个HTTP请求都必须承担一次握手延迟和一次挥手延迟的开销,这极大地影响了Web性能。这也是为什么后来的HTTP/1.1引入了持久连接(Keep-Alive),HTTP/2引入了多路复用等技术,其核心目的之一就是为了摊平TCP连接建立的成本。

TCP Fast Open (TFO):为速度而生的握手优化

有没有办法在保证安全性的前提下,减少甚至消除这次握手带来的延迟呢?答案是肯定的,这就是TCP Fast Open(TFO)技术,其标准化文档为 RFC 7413。

TFO的核心思想是:对于之前已经成功建立过连接的客户端,允许它在第一个SYN报文中就携带应用数据。 这相当于将数据传输和三次握手的第二、三步合并进行,从而实现“0-RTT”的数据交换。

TFO工作流程:

  1. 首次连接(获取Cookie):
    • 客户端在第一个SYN报文中,包含一个特殊的“Fast Open Cookie Request”TCP选项。
    • 服务器收到请求后,正常完成三次握手。在握手或后续的数据传输过程中,服务器会生成一个加密的Cookie(通常包含了客户端的IP地址等信息,并使用服务器密钥加密),并通过“Fast Open Cookie”TCP选项返回给客户端。
    • 客户端收到并缓存这个Cookie。这个Cookie在一段时间内是有效的。
  2. 后续连接(使用Cookie):
    • 当同一个客户端再次向该服务器发起连接时,它会在SYN报文中包含上次获取到的有效Cookie,并且紧跟着SYN报文之后,就可以直接发送应用层数据(例如一个HTTP GET请求)。
    • 服务器收到这个带有有效Cookie的SYN报文后,会验证Cookie的合法性。如果验证通过,服务器在发送SYN-ACK的同时,就已经可以开始处理SYN包中附带的数据了,将其递交给上层应用程序。
    • 之后,客户端收到SYN-ACK后,发送最终的ACK,握手完成。但此时,服务器可能已经处理完请求,并正在发送响应数据了。

通过TFO,对于重复访问的客户端,数据交换的延迟从一个RTT降低到了接近零。这对于API服务、网页加载等场景有着显著的性能提升。

TFO的设计同样考虑了安全性。通过加密的Cookie,服务器确保了只有之前合法通信过的客户端才能使用这个快速通道,防止了攻击者利用TFO发送带有数据的SYN包来进行DDoS攻击。服务器可以拒绝处理没有有效Cookie或者Cookie验证失败的SYN包中的数据。

目前,主流的操作系统(如Linux 3.7+)和Web服务器(如Nginx)都已经支持TFO,它正在成为现代高性能网络服务的一项标准配置。

第六章:从理论到实践 - 使用工具观察三次握手

理论知识的最终归宿是实践。我们可以使用强大的网络抓包工具,如 `tcpdump` 或 `Wireshark`,来亲眼观察网络中真实发生的三次握手过程。

假设我们想观察访问 `www.example.com` 时的TCP握手。我们可以在终端中使用 `tcpdump` 命令:


sudo tcpdump -i any -nS 'host www.example.com and port 80'

这条命令的含义是:

  • `sudo`: 需要管理员权限来监听网络接口。
  • `tcpdump`: 命令行抓包工具。
  • `-i any`: 监听所有网络接口。
  • `-n`: 不解析IP地址和端口号为主机名和服务名,直接显示数字。
  • `-S`: 显示绝对序列号。
  • `'host www.example.com and port 80'`: 这是一个过滤器,只显示与主机 `www.example.com` 且端口为80的流量。

当你执行这条命令后,在另一个终端窗口使用 `curl http://www.example.com`,你可能会看到类似下面这样的输出(IP地址和序列号每次都会不同):


# 第一次握手: 客户端 -> 服务器 (SYN)
10:30:01.123456 IP 192.168.1.10.54321 > 93.184.216.34.80: Flags [S], seq 1234567890, win 64240, options [mss 1460,sackOK,TS val 100 ecr 0,nop,wscale 7], length 0

# 第二次握手: 服务器 -> 客户端 (SYN-ACK)
10:30:01.167890 IP 93.184.216.34.80 > 192.168.1.10.54321: Flags [S.], seq 9876543210, ack 1234567891, win 65535, options [mss 1460,sackOK,TS val 200 ecr 100,nop,wscale 7], length 0

# 第三次握手: 客户端 -> 服务器 (ACK)
10:30:01.167950 IP 192.168.1.10.54321 > 93.184.216.34.80: Flags [.], ack 9876543211, win 502, options [nop,nop,TS val 101 ecr 200], length 0

让我们来解读这段真实的“对话”:

  1. 第一行 (SYN):
    • `192.168.1.10.54321 > 93.184.216.34.80`: 我们的客户端(IP 192.168.1.10,随机端口 54321)向服务器(IP 93.184.216.34,端口 80)发起请求。
    • `Flags [S]`: SYN标志位置为1。
    • `seq 1234567890`: 客户端的初始序列号 (ISN),这是一个巨大的随机数。
    • `length 0`: 这个包没有携带任何应用数据。
  2. 第二行 (SYN-ACK):
    • `93.184.216.34.80 > 192.168.1.10.54321`: 服务器回复客户端。
    • `Flags [S.]`: `S`代表SYN,`.`代表ACK,所以这是SYN-ACK标志位。
    • `seq 9876543210`: 服务器的初始序列号,也是一个巨大的随机数。
    • `ack 1234567891`: 确认号。它的值正好是客户端的ISN `1234567890` + 1。
  3. 第三行 (ACK):
    • `192.168.1.10.54321 > 93.184.216.34.80`: 客户端进行最终确认。
    • `Flags [.]`: `.`代表ACK标志位。
    • `ack 9876543211`: 确认号。它的值正好是服务器的ISN `9876543210` + 1。
    • 注意此时的 `seq` 应该是 `1234567891`,`tcpdump` 在某些模式下可能会省略显示没有变化的序列号。

通过这些真实的抓包数据,抽象的协议理论变得具体而生动。我们清晰地看到了序列号和确认号是如何在一次成功的握手中精确交换和确认的。这不仅验证了我们的理论知识,也为我们将来排查网络问题(如连接超时、连接被重置等)提供了最直接的工具和方法论。

结论:一场精心设计的对话

TCP三次握手,远不止是“SYN, SYN-ACK, ACK”的机械重复。它是一场精心设计的对话,每一个步骤,每一个标志位,每一个序列号,都承载着保证网络通信可靠性的深刻智慧。

  • 它通过三步确认,解决了网络延迟和报文丢失可能带来的“半开放连接”问题,确保了连接的真实有效性。
  • 它在操作系统内核中与状态机和连接队列紧密结合,构成了现代高并发网络服务的基础架构。
  • 它在面临SYN Flood等安全威胁时,催生了SYN Cookies这样精妙的防御机制,展现了网络安全攻防的持续演进。
  • 它在追求极致性能的道路上,通过TCP Fast Open等优化方案,不断突破自身的设计局限,适应着新时代应用的需求。

从最初的ARPANET到如今覆盖全球的互联网,TCP协议及其三次握手机制,历经数十年的考验,依然是数据通信领域不可动摇的基石。理解它,不仅仅是掌握了一个网络知识点,更是学会了从根本上欣赏那些在复杂和不确定的环境中构建可靠系统的设计哲学。这种思想,无论是在网络工程、分布式系统设计,还是在更广泛的软件工程领域,都将使我们受益匪浅。