icon

안동민 개발노트

6장 : TCP

연결 수립과 종료


TCP가 연결 지향이라는 것은, 데이터를 보내기 전에 반드시 연결을 먼저 설정한다는 의미였습니다. 이 연결 설정 과정이 3-way handshake이고, 연결 해제 과정이 4-way handshake입니다.

면접에서 가장 빈번하게 물어보는 주제 중 하나이며, 실무에서도 연결 관련 문제를 디버깅할 때 반드시 이해해야 하는 과정입니다.


3-way Handshake

TCP 연결 수립은 세 단계로 이루어집니다.

3-way Handshake 과정
Client                                          Server
  │                                               │
  │  [CLOSED]                            [LISTEN] │
  │                                               │
  │──── SYN (seq=1000) ───────────────────────→   │
  │  [SYN_SENT]                                   │
  │                                               │
  │   ←──── SYN-ACK (seq=3000, ack=1001) ──────   │
  │                              [SYN_RECEIVED]   │
  │                                               │
  │──── ACK (ack=3001) ──────────────────────→    │
  │  [ESTABLISHED]                [ESTABLISHED]   │
  │                                               │
  │      ← 이 시점부터 양방향 데이터 전송 가능 →  │
  │                                               │

1단계 — SYN: 클라이언트가 서버에게 "연결하고 싶습니다"라는 의사를 담은 SYN(Synchronize) 패킷을 보냅니다. 이 패킷에는 클라이언트의 초기 순서 번호(ISN: Initial Sequence Number)가 포함됩니다. 예를 들어 seq=1000이라고 합시다.

2단계 — SYN-ACK: 서버가 클라이언트의 요청을 수락하며, 두 가지를 동시에 보냅니다. 클라이언트의 SYN에 대한 확인(ACK)과 서버 자신의 SYN입니다. ack=1001(클라이언트의 seq+1)과 seq=3000(서버의 ISN)을 함께 보냅니다.

3단계 — ACK: 클라이언트가 서버의 SYN에 대한 확인(ACK)을 보냅니다. ack=3001(서버의 seq+1)을 보내면 연결이 수립됩니다. 이 세 번째 패킷부터는 데이터를 함께 실어 보낼 수 있습니다.

3-way일까요? 양쪽이 모두 상대방의 통신 능력을 확인해야 하기 때문입니다.

3-way가 필요한 이유
최소 확인 사항
  1. Client → Server: "내가 보낸 것이 너에게 도달하는가?" (SYN)
  2. Server → Client: "그래, 도달한다. 내가 보낸 것도 너에게 도달하는가?" (SYN-ACK)
  3. Client → Server: "그래, 도달한다." (ACK)

2-way로는 부족한 이유
  Client가 SYN을 보내고 Server가 SYN-ACK를 보내면...
  Server는 자신의 SYN이 Client에 도달했는지 알 수 없음
  → Client → Server 방향은 확인됨
  → Server → Client 방향은 미확인

ISN(Initial Sequence Number)의 비밀

ISN은 왜 0이나 1이 아니라 랜덤한 큰 수일까요? 보안과 신뢰성 때문입니다.

ISN이 랜덤인 이유
시나리오: ISN이 항상 0이라면

1차 연결: Client(seq=0) ←→ Server(seq=0)
  데이터 전송 중 패킷 일부가 네트워크에 잔류

연결 종료 후 같은 IP:Port로 재연결

2차 연결: Client(seq=0) ←→ Server(seq=0)
  1차 연결의 잔류 패킷이 도착 → 2차 연결의 데이터로 오인!

ISN이 랜덤이면
  1차 연결: seq=157482...
  2차 연결: seq=892347...
  → 잔류 패킷의 seq가 2차 연결 범위에 없으므로 폐기됨

보안 측면
  ISN이 예측 가능하면 → TCP Sequence Prediction Attack
  공격자가 seq를 추측하여 위조 패킷 삽입 가능
  → RFC 6528: ISN은 암호학적으로 안전한 랜덤 생성 권장

4-way Handshake

TCP 연결 종료는 네 단계로 이루어집니다. 양쪽이 각각 독립적으로 연결을 종료해야 하기 때문에, 연결보다 한 단계가 더 필요합니다.

4-way Handshake 과정
Client                                          Server
  │                                               │
  │  [ESTABLISHED]                [ESTABLISHED]   │
  │                                               │
  │──── FIN (seq=5000) ──────────────────────→    │
  │  [FIN_WAIT_1]                                 │
  │                                               │
  │   ←──── ACK (ack=5001) ────────────────────   │
  │  [FIN_WAIT_2]                [CLOSE_WAIT]     │
  │                                               │
  │          (Server가 남은 데이터 전송 가능)     │
  │                                               │
  │   ←──── FIN (seq=7000) ────────────────────   │
  │                              [LAST_ACK]       │
  │                                               │
  │──── ACK (ack=7001) ──────────────────────→    │
  │  [TIME_WAIT]                  [CLOSED]        │
  │                                               │
  │  (2MSL 대기 후)                               │
  │  [CLOSED]                                     │

1단계 — FIN: 연결을 먼저 끊으려는 쪽(보통 클라이언트)이 FIN(Finish) 패킷을 보냅니다. "더 이상 보낼 데이터가 없습니다"라는 의미입니다.

2단계 — ACK: 상대방이 FIN을 받았다는 확인(ACK)을 보냅니다. 하지만 이 시점에서 상대방은 아직 보낼 데이터가 남아 있을 수 있습니다.

3단계 — FIN: 상대방도 보낼 데이터를 모두 전송한 후, 자신의 FIN을 보냅니다. "나도 더 이상 보낼 데이터가 없습니다."

4단계 — ACK: 처음 FIN을 보냈던 쪽이 상대방의 FIN에 대한 확인(ACK)을 보냅니다. 이 ACK가 전달되면 연결이 완전히 종료됩니다.

왜 3-way가 아니라 4-way일까요? 연결 수립 시에는 서버가 SYN과 ACK를 하나의 패킷(SYN-ACK)으로 합칠 수 있었습니다. 하지만 종료 시에는 한쪽이 FIN을 보내도 상대방은 아직 보낼 데이터가 남아 있을 수 있으므로, ACK와 FIN을 분리해야 합니다.


TCP 연결 상태 전이

TCP 연결의 전체 상태 전이를 정리합니다.

TCP 상태 전이도
                        Active Open        Passive Open
                        (Client)           (Server)
                            │                  │
                            ▼                  ▼
                        ┌────────┐         ┌────────┐
                  ┌────→│ CLOSED │←────────│ CLOSED │
                  │     └───┬────┘         └───┬────┘
                  │         │ SYN 전송         │ listen()
                  │         ▼                  ▼
                  │     ┌──────────┐       ┌────────┐
                  │     │ SYN_SENT │       │ LISTEN │
                  │     └────┬─────┘       └───┬────┘
                  │          │ SYN-ACK 수신    │ SYN 수신
                  │          │ ACK 전송        │ SYN-ACK 전송
                  │          ▼                 ▼
                  │     ┌──────────────────────────┐
                  │     │      ESTABLISHED         │
                  │     └──────┬───────────┬────────┘
                  │            │           │
                  │    FIN 전송│          │FIN 수신
                  │            ▼           ▼
                  │     ┌───────────┐ ┌───────────┐
                  │     │FIN_WAIT_1 │ │CLOSE_WAIT │
                  │     └─────┬─────┘ └──────┬────┘
                  │    ACK수신│       FIN전송│
                  │           ▼              ▼
                  │     ┌───────────┐ ┌───────────┐
                  │     │FIN_WAIT_2 │ │ LAST_ACK  │
                  │     └─────┬─────┘ └──────┬────┘
                  │    FIN수신│       ACK수신│
                  │           ▼              │
                  │     ┌───────────┐        │
                  │     │ TIME_WAIT │        │
                  │     └─────┬─────┘        │
                  │      2MSL 타임아웃       │
                  └───────────┴──────────────┘

TIME_WAIT 상태

연결 종료에서 가장 주의해야 할 부분이 TIME_WAIT 상태입니다.

마지막 ACK를 보낸 쪽(먼저 FIN을 보낸 쪽)은 연결을 즉시 폐기하지 않고, 일정 시간(보통 2MSL, 약 60초) 동안 TIME_WAIT 상태를 유지합니다.

TIME_WAIT가 필요한 이유
이유 1: 마지막 ACK 손실 대비
  Client ──── ACK ────→ Server

              (ACK가 손실되면?)

              Server: ACK가 안 왔네, FIN 재전송!

  Client ←──── FIN ──── Server

    └── TIME_WAIT 상태이므로 FIN에 대해 ACK 재전송 가능
        만약 CLOSED였다면 → RST로 응답 → Server 혼란

이유 2: 지연 패킷 격리
  이전 연결: 192.168.1.10:50000 ↔ 10.0.0.1:80
    잔류 패킷이 네트워크 어딘가에 떠돌고 있음

  TIME_WAIT 없이 즉시 같은 4-tuple로 새 연결 생성:
    새 연결: 192.168.1.10:50000 ↔ 10.0.0.1:80
    잔류 패킷 도착 → 새 연결의 데이터로 오인!

  TIME_WAIT (60초) 동안 대기
    잔류 패킷은 TTL 만료로 폐기됨
    안전하게 같은 4-tuple 재사용 가능

TIME_WAIT 실무 문제와 해결

고트래픽 서버에서 TIME_WAIT가 누적되면 심각한 문제가 됩니다.

time_wait_check.sh
#!/bin/bash
# === TIME_WAIT 모니터링 ===

echo "=== TCP 상태별 연결 수 ==="
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

echo ""
echo "=== TIME_WAIT 연결 수 ==="
ss -tan state time-wait | wc -l

echo ""
echo "=== 현재 TCP 관련 커널 파라미터 ==="
echo "tcp_tw_reuse: $(cat /proc/sys/net/ipv4/tcp_tw_reuse)"
echo "tcp_fin_timeout: $(cat /proc/sys/net/ipv4/tcp_fin_timeout)"
echo "ip_local_port_range: $(cat /proc/sys/net/ipv4/ip_local_port_range)"

echo ""
echo "=== 권장 설정 (고트래픽 서버) ==="
# TIME_WAIT 소켓 재사용 허용
# sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# FIN_WAIT 타임아웃 단축 (기본 60초 → 30초)
# sudo sysctl -w net.ipv4.tcp_fin_timeout=30

# 임시 포트 범위 확대 (기본: 32768~60999)
# sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
해결 방법설명주의사항
tcp_tw_reuse=1TIME_WAIT 소켓을 새 연결에 재사용timestamp 옵션 필요
Connection Pool연결을 재사용하여 생성/종료 최소화DB, HTTP 클라이언트에 필수
Keep-Alive하나의 연결로 여러 요청 처리HTTP/1.1 기본 동작
포트 범위 확대사용 가능한 임시 포트 수 증가근본 해결은 아님
SO_LINGER(0)TIME_WAIT 없이 RST로 즉시 종료데이터 손실 위험, 권장하지 않음

비정상 종료: RST

4-way handshake는 정상적인 연결 종료 방식입니다. 하지만 비정상적인 상황에서는 RST(Reset) 패킷을 통해 연결을 즉시 끊습니다.

RST 발생 시나리오
시나리오 1: 닫힌 포트로 연결 시도
  Client ──── SYN (dst port 9999) ────→ Server
  Client ←──── RST ────────────────── Server
  "9999 포트에서 리슨하는 프로세스 없음"

시나리오 2: 서버 프로세스 비정상 종료
  연결 중 서버 프로세스 crash
  OS가 해당 연결에 RST 전송
  Client ←──── RST ────────────────── Server (OS)

시나리오 3: 방화벽 차단
  Client ──── SYN ────→ [Firewall] ──× ── Server
  Client ←──── RST ──── [Firewall]
  "방화벽이 RST로 차단 응답"

시나리오 4: 반개방 연결 감지
  Server 재부팅 후, Client가 기존 연결에 데이터 전송
  Server ←──── Data (seq=5000) ──── Client
  Server는 이 연결을 모름 → RST 응답

RST가 발생하는 대표적인 경우들입니다.

  • 존재하지 않는 포트로 연결을 시도한 경우: 서버에서 해당 포트를 리슨하고 있지 않으면 RST로 응답합니다.

  • 서버 프로세스가 예기치 않게 종료된 경우: 운영체제가 해당 연결에 대해 RST를 보냅니다.

  • 방화벽이 연결을 차단하는 경우: 일부 방화벽은 RST로 응답하여 연결을 거부합니다.

  • 애플리케이션이 소켓을 강제로 닫는 경우: SO_LINGER 옵션을 사용하여 FIN 대신 RST로 즉시 종료할 수 있습니다.

RST는 4-way handshake를 거치지 않고 연결을 즉시 폐기합니다. TIME_WAIT도 발생하지 않습니다. 빠르지만, 상대방에게 미처 전달하지 못한 데이터가 손실될 수 있으므로 신중하게 사용해야 합니다.


연결 수립/종료 실습

connection_states.py
import socket
import time
import subprocess
import sys

def show_connection_state(description):
    """현재 TCP 연결 상태를 출력"""
    print(f"\n--- {description} ---")
    if sys.platform == "win32":
        result = subprocess.run(
            ["netstat", "-an"],
            capture_output=True, text=True
        )
        for line in result.stdout.splitlines():
            if "8765" in line:
                print(f"  {line.strip()}")
    else:
        result = subprocess.run(
            ["ss", "-tan"],
            capture_output=True, text=True
        )
        for line in result.stdout.splitlines():
            if "8765" in line:
                print(f"  {line.strip()}")

# TCP 서버 시작
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 8765))
server.listen(1)
show_connection_state("서버 LISTEN 상태")

# 클라이언트 연결 (3-way handshake 발생)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8765))
conn, addr = server.accept()
show_connection_state("ESTABLISHED 상태")

# 데이터 교환
client.sendall(b"Hello TCP!")
data = conn.recv(1024)
print(f"\n수신 데이터: {data.decode()}")

# 클라이언트가 먼저 종료 (4-way handshake 시작)
client.close()
show_connection_state("클라이언트 FIN 전송 후")

time.sleep(0.1)

# 서버도 종료
conn.close()
show_connection_state("양쪽 모두 종료 후 (TIME_WAIT)")

server.close()

연결 관련 면접 질문 정리

질문핵심 답변
3-way handshake를 설명하세요SYN → SYN-ACK → ACK, 양방향 통신 능력 상호 확인
왜 2-way가 아니라 3-way인가요?Server→Client 방향 확인에 3번째 ACK 필요
4-way handshake를 설명하세요FIN → ACK → FIN → ACK, 양쪽 독립적 종료
왜 3-way가 아니라 4-way인가요?상대방의 ACK와 FIN 사이에 잔여 데이터 전송 가능
TIME_WAIT의 목적은?마지막 ACK 손실 대비 + 지연 패킷 격리
CLOSE_WAIT이 누적되면?애플리케이션이 close()를 호출하지 않는 버그
RST와 FIN의 차이는?FIN은 정상 종료(데이터 완전 전송), RST는 즉시 폐기
SYN Flood 공격이란?대량 SYN 전송 + ACK 미응답 = 서버 자원 고갈

다음 절에서는 TCP가 데이터를 안정적으로 전송하기 위한 핵심 메커니즘인 흐름 제어와 혼잡 제어를 살펴보겠습니다.

목차