본문으로 건너뛰기 TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

이 글의 핵심

TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드.

들어가며: TCP 상태를 이해해야 하는 이유

네트워크 애플리케이션을 개발하거나 디버깅할 때 TCP 연결 상태를 이해하는 것은 필수입니다. netstat 명령어로 본 ESTABLISHED, TIME_WAIT, CLOSE_WAIT 같은 상태들이 무엇을 의미하는지, 언제 발생하는지 알아야 성능 문제와 버그를 해결할 수 있습니다. 이 글에서 다룰 내용

  • TCP 11가지 연결 상태
  • 3-Way Handshake와 4-Way Handshake
  • 상태 전이 다이어그램
  • netstat/ss로 상태 확인
  • 실전 디버깅 시나리오

1. TCP 연결 상태 개요

TCP 상태 머신

TCP는 상태 기반 프로토콜입니다. 각 소켓은 11가지 상태 중 하나에 있으며, 패킷 송수신에 따라 상태가 전이됩니다.

11가지 TCP 상태

상태설명발생 시점
CLOSED연결 없음초기 상태
LISTEN연결 대기 중서버가 포트를 열고 대기
SYN_SENT연결 요청 전송클라이언트가 SYN 전송 후
SYN_RECEIVED연결 요청 수신서버가 SYN 받고 SYN-ACK 전송
ESTABLISHED연결 수립 완료3-Way Handshake 완료
FIN_WAIT_1종료 요청 전송능동적 종료 시작 (FIN 전송)
FIN_WAIT_2종료 요청 승인 대기FIN에 대한 ACK 수신
CLOSE_WAIT종료 요청 수신상대방이 FIN 전송 (수동적 종료)
CLOSING동시 종료양이 동시에 FIN 전송
LAST_ACK최종 ACK 대기CLOSE_WAIT에서 FIN 전송 후
TIME_WAIT연결 종료 대기능동적 종료 완료, 2MSL 대기

2. 연결 수립: 3-Way Handshake

왜 3-Way인가? (2-Way가 아니라)

TCP는 양방향 신뢰성 있는 통신을 제공합니다. 3-Way Handshake는 다음을 보장합니다:

  1. 클라이언트 → 서버 경로 확인: SYN
  2. 서버 → 클라이언트 경로 확인: SYN-ACK
  3. 양방향 통신 준비 완료: ACK

2-Way가 불충분한 이유:

2-Way 시나리오 (불안전):
  Client → Server: SYN
  Server → Client: SYN-ACK
  
문제:
- 서버는 SYN-ACK를 보냈지만, 클라이언트가 받았는지 알 수 없음
- 만약 SYN-ACK가 손실되면?
  → 서버는 ESTABLISHED 상태로 전환
  → 클라이언트는 여전히 SYN_SENT
  → 서버가 데이터를 보내도 클라이언트가 받지 못함
  
3-Way 해결책:
  Client → Server: SYN
  Server → Client: SYN-ACK
  Client → Server: ACK ← 이것이 핵심!
  
- ACK를 받은 서버만 ESTABLISHED로 전환
- 클라이언트도 SYN-ACK를 받았으므로 ESTABLISHED
- 양방향 통신 확실히 가능

3-Way Handshake 과정

sequenceDiagram
    participant Client
    participant Server
    
    Note over Client: CLOSED
    Note over Server: LISTEN
    
    Client->>Server: 1. SYN (seq=100)
    Note over Client: SYN_SENT
    Note right of Client: 초기 시퀀스 번호(ISN) 전송
커널이 랜덤 생성 Server->>Client: 2. SYN-ACK (seq=200, ack=101) Note over Server: SYN_RECEIVED Note left of Server: 서버의 ISN + 클라이언트 ACK
백로그 큐에 추가 Client->>Server: 3. ACK (ack=201) Note over Client: ESTABLISHED Note over Server: ESTABLISHED Note right of Client: accept() 큐로 이동
애플리케이션이 처리 가능 Note over Client,Server: 데이터 전송 가능

상태 전이 및 커널 동작

클라이언트: CLOSED → SYN_SENT → ESTABLISHED
서버:       CLOSED → LISTEN → SYN_RECEIVED → ESTABLISHED

클라이언트 측 커널 동작:

// socket() 호출: TCB (Transmission Control Block) 할당
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 상태: CLOSED
// 커널: TCB 생성, 소켓 구조체 초기화

// connect() 호출: SYN 전송
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 1. 초기 시퀀스 번호(ISN) 생성: 현재 시간 + 난수
//    보안: 예측 불가능한 ISN으로 SYN Flood 방어
// 2. SYN 패킷 생성: 
//    TCP 헤더: SYN 플래그 설정, seq = ISN
// 3. IP 계층으로 전달 → 네트워크로 송신
// 4. 상태: CLOSED → SYN_SENT
// 5. 재전송 타이머 시작 (보통 1초, 2초, 4초... 최대 75초)
// 6. SYN-ACK 대기 (connect()는 블로킹)

// SYN-ACK 수신 시:
// 1. ack 값 검증: ack == ISN + 1인지 확인
// 2. ACK 패킷 생성 및 전송
// 3. 상태: SYN_SENT → ESTABLISHED
// 4. connect() 반환 (블로킹 해제)

서버 측 커널 동작:

// socket() + bind() + listen() 호출
int server = socket(AF_INET, SOCK_STREAM, 0);
bind(server, (struct sockaddr*)&addr, sizeof(addr));
listen(server, 128);  // 백로그 큐 크기
// 상태: CLOSED → LISTEN
// 커널: 
//   - SYN 큐 (미완성 연결, 크기: tcp_max_syn_backlog)
//   - Accept 큐 (완성된 연결, 크기: listen backlog)

// SYN 수신 시:
// 1. SYN 큐에 공간 있는지 확인
//    - 가득 차면: SYN Cookies 사용 또는 드롭
// 2. 반-완성 연결 생성 (Half-Open Connection)
// 3. 서버의 ISN 생성
// 4. SYN-ACK 패킷 생성 및 전송:
//    seq = 서버 ISN
//    ack = 클라이언트 ISN + 1
// 5. 상태: LISTEN → SYN_RECEIVED
// 6. 재전송 타이머 시작 (SYN-ACK 재전송)

// ACK 수신 시:
// 1. SYN 큐에서 제거
// 2. Accept 큐로 이동
// 3. 상태: SYN_RECEIVED → ESTABLISHED
// 4. accept() 호출 시 Accept 큐에서 꺼냄
//    - 큐가 비어있으면 accept()는 블로킹

int client = accept(server, ...);
// accept() 반환: Accept 큐에서 ESTABLISHED 소켓 반환

백로그 큐 관리:

          [SYN 큐]              [Accept 큐]
            ↓                       ↓
┌──────────────────────┐  ┌─────────────────────┐
│ SYN_RECEIVED 연결들  │→│ ESTABLISHED 연결들   │→ accept()
│ (미완성)             │  │ (완성, 대기 중)      │
│ tcp_max_syn_backlog  │  │ listen backlog      │
└──────────────────────┘  └─────────────────────┘

SYN Flood 공격 시:
- SYN 큐가 가득 참 → 새 SYN 드롭
- 정상 클라이언트도 연결 불가 (DoS)

해결책:
1. SYN Cookies 활성화
   - SYN 큐 우회, 무상태(Stateless) 처리
   - ISN에 연결 정보 인코딩
   
2. 백로그 증가
   sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192
   sudo sysctl -w net.core.somaxconn=4096

시퀀스 번호(Sequence Number)의 중요성:

왜 랜덤 ISN을 사용하나?

고정 ISN (예: 0)의 위험:
1. 이전 연결의 지연된 패킷이 새 연결에 섞일 수 있음
2. 공격자가 ISN을 예측하여 위조 패킷 전송 가능

랜덤 ISN (RFC 6528):
  ISN = M + F(src_ip, src_port, dst_ip, dst_port, secret)
  
  M = 4 마이크로초마다 증가하는 카운터
  F = 해시 함수 (MD5 또는 SHA-1)
  secret = 부팅 시 생성된 랜덤 키
  
→ 32비트 공간을 약 4.55시간에 순환
→ 예측 불가능 (보안)
→ 이전 연결과 충돌 방지

Python으로 3-Way Handshake 관찰

import socket
import time
def client_connect():
    """클라이언트 연결 과정"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    print("📍 상태: CLOSED")
    
    # connect() 호출 시 SYN 전송
    print("📤 SYN 전송 중...")
    print("📍 상태: SYN_SENT")
    
    sock.connect(('example.com', 80))
    
    print("✅ 연결 수립")
    print("📍 상태: ESTABLISHED")
    
    return sock
def server_listen():
    """서버 리스닝 과정"""
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    server.bind(('0.0.0.0', 8080))
    
    print("📍 상태: CLOSED")
    
    server.listen(5)
    print("👂 포트 8080에서 대기 중...")
    print("📍 상태: LISTEN")
    
    client, addr = server.accept()
    print(f"✅ {addr}로부터 연결 수립")
    print("📍 상태: ESTABLISHED")
    
    return client

패킷 레벨 분석

# tcpdump로 3-Way Handshake 캡처
sudo tcpdump -i any -nn 'tcp port 80' -c 3
# 출력:
# 1. 192.168.1.100.54321 > 93.184.216.34.80: Flags [S], seq 100
# 2. 93.184.216.34.80 > 192.168.1.100.54321: Flags [S.], seq 200, ack 101
# 3. 192.168.1.100.54321 > 93.184.216.34.80: Flags [.], ack 201

3. 연결 종료: 4-Way Handshake

정상 종료 (4-Way Handshake)

sequenceDiagram
    participant Client as Client\n(능동적 종료)
    participant Server as Server\n(수동적 종료)
    
    Note over Client,Server: ESTABLISHED
    
    Client->>Server: 1. FIN (seq=300)
    Note over Client: FIN_WAIT_1
    
    Server->>Client: 2. ACK (ack=301)
    Note over Client: FIN_WAIT_2
    Note over Server: CLOSE_WAIT
    
    Note over Server: 애플리케이션이\nclose() 호출할 때까지 대기
    
    Server->>Client: 3. FIN (seq=400)
    Note over Server: LAST_ACK
    
    Client->>Server: 4. ACK (ack=401)
    Note over Client: TIME_WAIT
    Note over Server: CLOSED
    
    Note over Client: 2MSL (60초) 대기
    Note over Client: CLOSED

능동적 종료 vs 수동적 종료

# 능동적 종료 (Active Close)
# - close()를 먼저 호출하는 쪽
# - FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
client_socket.close()  # 클라이언트가 먼저 종료
# 상태: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
# 수동적 종료 (Passive Close)
# - 상대방의 FIN을 받는 쪽
# - CLOSE_WAIT → LAST_ACK → CLOSED
# 서버는 클라이언트의 FIN을 받음
# 상태: ESTABLISHED → CLOSE_WAIT
# 애플리케이션이 close() 호출
server_socket.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED

4. 11가지 TCP 상태 상세 설명

1. CLOSED

초기 상태. 연결이 존재하지 않음.

2. LISTEN

서버가 특정 포트에서 연결 요청을 기다리는 상태.
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)  # 백로그 큐 크기 5
# 이 시점에서 상태: LISTEN
print("Server is LISTEN on port 8080")
# 확인
netstat -an | grep 8080
# tcp4  0  0  *.8080  *.*  LISTEN

3. SYN_SENT

클라이언트가 SYN 패킷을 전송하고 SYN-ACK를 기다리는 상태.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)  # 논블로킹 모드
try:
    sock.connect(('example.com', 80))
except BlockingIOError:
    # 이 시점에서 상태: SYN_SENT
    print("Connection in progress (SYN_SENT)")
    
    # select로 연결 완료 대기
    import select
    _, writable, _ = select.select([], [sock], [], 5)
    
    if writable:
        print("Connection ESTABLISHED")

SYN_SENT가 오래 유지되는 경우:

  • 방화벽이 SYN 패킷 차단
  • 서버가 다운되어 응답 없음
  • 네트워크 지연

4. SYN_RECEIVED

서버가 SYN을 받고 SYN-ACK를 전송한 후, 최종 ACK를 기다리는 상태.

SYN Flood 공격 상세 분석:

flowchart LR
    Attacker[공격자] -->|대량 SYN| Server[서버]
    Server -->|SYN-ACK| Void[응답 없음]
    
    Note1["서버의 SYN_RECEIVED\n큐가 가득 참"]
    
    style Attacker fill:#ff6b6b
    style Void fill:#ff6b6b

SYN Flood 공격 내부 메커니즘:

일반 연결:
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK ← 정상 완료
Server: SYN 큐에서 Accept 큐로 이동

SYN Flood 공격:
Attacker → Server: SYN (위조 IP: 1.1.1.1)
Server → 1.1.1.1: SYN-ACK  ← 응답 없음 (위조 IP)
Server: SYN 큐에 SYN_RECEIVED 상태로 대기

Attacker → Server: SYN (위조 IP: 2.2.2.2)
Server → 2.2.2.2: SYN-ACK  ← 응답 없음
Server: SYN 큐에 추가

... 수천 개 반복 ...

Server: SYN 큐 가득 참 (1024개)
→ 정상 클라이언트 SYN도 드롭
→ 서비스 거부 (DoS)

SYN 큐 메모리 소비:
각 SYN_RECEIVED 연결:
- TCB (Transmission Control Block): ~280 bytes
- 재전송 타이머
- 소켓 구조체

1024개 × 280 bytes = ~280KB (작지만 리소스 고갈)

재전송 타이머:
1초 → 2초 → 4초 → 8초 → 16초 → 32초
총 63초 동안 SYN 큐 점유

공격 효과:
- 초당 1000개 SYN
- 각각 63초 유지
- 필요 큐 크기: 63,000개
- 서버 마비

SYN Cookies 방어 메커니즘:

SYN Cookies 원리 (RFC 4987):

일반 방식 (상태 유지):
1. SYN 수신 → TCB 할당 → SYN 큐 저장
2. SYN-ACK 전송
3. ACK 수신 → TCB 조회 → Accept 큐 이동

문제: SYN 큐 메모리 소비 (공격에 취약)

SYN Cookies 방식 (무상태):
1. SYN 수신 → TCB 할당 안 함!
2. ISN 계산 (Stateless):
   
   ISN = hash(
     src_ip, src_port,
     dst_ip, dst_port,
     timestamp,  ← 시간 정보
     secret      ← 서버 비밀키
   ) + MSS_encoding + timestamp_encoding
   
   ISN 구조 (32 bits):
   [31:27] timestamp (5 bits, 32초 순환)
   [26:24] MSS encoding (3 bits, 8가지 MSS)
   [23:0]  hash 값 (24 bits)

3. SYN-ACK 전송 (seq=ISN)
   → SYN 큐에 저장 안 함! (메모리 절약)

4. ACK 수신 시:
   - ack 값에서 정보 복원:
     * ack - 1 = ISN
     * ISN에서 timestamp, MSS 추출
     * hash 재계산하여 검증
   
   검증:
   calculated_hash = hash(...)
   if (ack - 1) matches calculated_hash:
     → 정상 연결
     → ESTABLISHED 생성
   else:
     → 위조 패킷
     → 드롭

장점:
- SYN 큐 불필요 (메모리 무제한)
- SYN Flood 공격 무효화

단점:
- TCP 옵션 손실 (Window Scale, SACK 등)
- MSS만 8가지로 제한
- 성능 약간 저하

활성화:
Linux:
  net.ipv4.tcp_syncookies=1
  
  동작:
  - SYN 큐 < 75%: 일반 방식
  - SYN 큐 ≥ 75%: SYN Cookies 자동 활성화

확인:
$ netstat -s | grep "SYNs to LISTEN"
  123 SYNs to LISTEN sockets dropped
  456 times used SYN cookies

백로그 큐 (Backlog Queue) 상세:

listen(fd, backlog) 내부 구조:

Linux 커널 큐 2개:

1. SYN Queue (미완성 연결):
   크기: net.ipv4.tcp_max_syn_backlog (기본 1024)
   상태: SYN_RECEIVED
   
   [SYN1] [SYN2] [SYN3] ... [SYNn]

   각 항목:
   - 클라이언트 IP:Port
   - ISN
   - 재전송 타이머
   - TCP 옵션

2. Accept Queue (완성된 연결):
   크기: min(backlog, net.core.somaxconn)
   상태: ESTABLISHED
   
   [CONN1] [CONN2] [CONN3] ... [CONNn]

   accept() 호출 시 꺼냄

흐름:
SYN 수신 → SYN Queue 추가 (SYN_RECEIVED)
ACK 수신 → Accept Queue 이동 (ESTABLISHED)
accept() → Accept Queue에서 제거

큐 오버플로우:
SYN Queue 가득:
  - SYN Cookies 활성화 (tcp_syncookies=1)
  - 또는 SYN 드롭
  
Accept Queue 가득:
  - 새 ACK 무시
  - 클라이언트는 SYN-ACK 재전송 대기
  - 클라이언트 타임아웃 발생

튜닝:
# SYN 큐 증가
net.ipv4.tcp_max_syn_backlog=8192

# Accept 큐 증가
net.core.somaxconn=4096

# 애플리케이션
listen(fd, 4096)  # 큐 크기 명시

확인:
ss -lnt
State    Recv-Q  Send-Q  Local Address:Port
LISTEN   0       128     0.0.0.0:80
         ↑       ↑
    Accept큐    Accept큐 최대 크기
    현재 대기   (listen backlog)
# SYN_RECEIVED 상태 확인
netstat -an | grep SYN_RECV

# SYN Flood 방어 (Linux)
sudo sysctl -w net.ipv4.tcp_syncookies=1
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192

# 백로그 확인
ss -lnt

5. ESTABLISHED

연결이 수립되어 데이터를 주고받을 수 있는 상태.
# ESTABLISHED 상태에서 데이터 송수신
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))
# 상태: ESTABLISHED
sock.send(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
response = sock.recv(4096)
print(response.decode())
# ESTABLISHED 연결 수 확인
netstat -an | grep ESTABLISHED | wc -l
# 특정 포트의 ESTABLISHED 연결
netstat -an | grep ':80.*ESTABLISHED'

6. FIN_WAIT_1

능동적 종료를 시작하여 FIN을 전송한 상태. ACK를 기다림.
# 클라이언트가 먼저 종료
client_socket.close()  # FIN 전송
# 상태: ESTABLISHED → FIN_WAIT_1
# 서버의 ACK 대기

7. FIN_WAIT_2

FIN에 대한 ACK를 받았지만, 상대방의 FIN을 기다리는 상태.
sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: ESTABLISHED
    
    C->>S: FIN
    Note over C: FIN_WAIT_1
    
    S->>C: ACK
    Note over C: FIN_WAIT_2
    Note over S: CLOSE_WAIT
    
    Note over S: 서버가 close() 호출할 때까지\nFIN_WAIT_2 유지

FIN_WAIT_2 타임아웃:

# Linux에서 FIN_WAIT_2 타임아웃 설정 (기본 60초)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

8. CLOSE_WAIT - 가장 흔한 소켓 리소스 누수의 원인

상대방이 FIN을 보내 연결 종료를 요청한 상태.
애플리케이션이 close()를 호출해야 함.

CLOSE_WAIT vs TIME_WAIT 핵심 차이:

TIME_WAIT:
- 능동적 종료 측 (close()를 먼저 호출)
- 커널이 자동 관리
- 60초 후 자동 정리
- 정상적인 상태 (걱정 불필요)

CLOSE_WAIT:
- 수동적 종료 측 (FIN을 먼저 받음)
- 애플리케이션이 명시적으로 close() 호출 필요
- 애플리케이션이 close()를 안 부르면 영원히 유지
- 버그의 신호! (리소스 누수)

CLOSE_WAIT 발생 메커니즘:

# 서버 코드
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)

client_sock, addr = server.accept()
# 상태: ESTABLISHED

# 클라이언트가 연결을 닫음 (FIN 전송)
# 커널이 자동으로 ACK 응답
# 상태: ESTABLISHED → CLOSE_WAIT

# ⚠️ 문제: 애플리케이션이 close()를 호출하지 않음
# 상태: CLOSE_WAIT (영구 유지)
# 소켓 파일 디스크립터 누수
# 메모리 누수
# 포트 고갈 (서버 측에서는 문제 적지만)

# ✅ 해결: 반드시 close() 호출
client_sock.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED

CLOSE_WAIT 문제 (소켓 리소스 누수):

# ❌ 나쁜 예 1: Exception 발생 시 close() 누락
def handle_request(sock):
    data = sock.recv(1024)
    if not data:
        return  # ❌ close() 호출 안 함
    process(data)
    sock.send(b'OK')
    sock.close()  # ❌ 여기 도달하지 못할 수 있음

# ❌ 나쁜 예 2: 무한 루프에서 close() 누락
def handle_client(sock):
    while True:
        data = sock.recv(1024)
        if not data:
            break  # ❌ close() 없이 종료
        process(data)

# ❌ 나쁜 예 3: 멀티스레드에서 소켓 관리 실패
def worker(sock):
    try:
        process(sock)
    except Exception as e:
        log_error(e)
        # ❌ Exception 발생 시 close() 누락
    
# ✅ 좋은 예 1: try-finally
def handle_request(sock):
    try:
        data = sock.recv(1024)
        if not data:
            return
        process(data)
        sock.send(b'OK')
    finally:
        sock.close()  # ✅ 항상 실행됨

# ✅ 좋은 예 2: Context Manager
def handle_request(sock):
    with sock:  # ✅ __exit__에서 자동 close()
        data = sock.recv(1024)
        process(data)
        sock.send(b'OK')

# ✅ 좋은 예 3: recv(0) 감지
def handle_client(sock):
    try:
        while True:
            data = sock.recv(1024)
            if not data:  # FIN 수신 (상대방이 닫음)
                break
            process(data)
    finally:
        sock.close()  # ✅ 반드시 닫기

CLOSE_WAIT 디버깅 실전 가이드:

# 1. CLOSE_WAIT 소켓 수 확인
netstat -an | grep CLOSE_WAIT | wc -l
# 100개 이상이면 문제!

# 2. 프로세스별 소켓 수 확인
lsof -p <PID> | grep TCP | wc -l
# 또는
ls -l /proc/<PID>/fd | grep socket | wc -l

# 3. 어떤 소켓이 CLOSE_WAIT인지 확인
sudo ss -tanp | grep CLOSE_WAIT
# 출력 예시:
# CLOSE-WAIT  0   0   192.168.1.100:8080  192.168.1.200:54321  users:(("python",pid=1234,fd=5))
#                     ↑ 서버 포트           ↑ 클라이언트 포트    ↑ 프로세스 정보

# 4. 특정 프로세스의 소켓 상태
sudo lsof -p <PID> -a -i TCP
# 출력 예시:
# python 1234 user  5u  IPv4 12345  TCP *:8080 (LISTEN)
# python 1234 user  6u  IPv4 12346  TCP 192.168.1.100:8080->192.168.1.200:54321 (CLOSE_WAIT)
#                                                                                 ↑ 문제!

# 5. 파일 디스크립터 한계 확인
ulimit -n
# 1024 ← 기본값
# CLOSE_WAIT가 1024개 쌓이면 새 연결 불가!

# 6. 커널 소켓 통계
cat /proc/net/sockstat
# 출력:
# TCP: inuse 150 orphan 0 tw 50 alloc 200 mem 10
#      ↑ 사용 중   ↑ 고아       ↑ TIME_WAIT  ↑ 할당됨

실제 프로덕션 사례:

# 사례 1: Django 뷰에서 외부 API 호출
import requests

def my_view(request):
    response = requests.get('https://api.example.com/data')
    # ❌ 문제: requests는 내부적으로 소켓 사용
    # Connection: close로 응답 시 서버가 먼저 FIN 전송
    # requests가 제대로 정리 안 하면 CLOSE_WAIT 누적
    
    # ✅ 해결책 1: Session 사용 (연결 재사용)
    session = requests.Session()
    response = session.get('https://api.example.com/data')
    session.close()  # 명시적 종료
    
    # ✅ 해결책 2: Context Manager
    with requests.Session() as session:
        response = session.get('https://api.example.com/data')

# 사례 2: 데이터베이스 연결 누수
import psycopg2

def query_database():
    conn = psycopg2.connect(
        host='localhost',
        database='mydb',
        user='user',
        password='pass'
    )
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    results = cursor.fetchall()
    # ❌ conn.close() 누락!
    # PostgreSQL 서버에서 CLOSE_WAIT 누적
    
    # ✅ 해결책
    try:
        # ... 쿼리 실행 ...
    finally:
        cursor.close()
        conn.close()

# 사례 3: WebSocket 연결 관리
import websocket

def handle_websocket():
    ws = websocket.create_connection('ws://server.com/socket')
    ws.send('Hello')
    result = ws.recv()
    # ❌ ws.close() 누락!
    
    # ✅ 해결책
    try:
        ws.send('Hello')
        result = ws.recv()
    finally:
        ws.close()

CLOSE_WAIT 모니터링 자동화:

#!/bin/bash
# close_wait_monitor.sh

THRESHOLD=50
CLOSE_WAIT_COUNT=$(netstat -an | grep CLOSE_WAIT | wc -l)

if [ $CLOSE_WAIT_COUNT -gt $THRESHOLD ]; then
    echo "⚠️  ALERT: CLOSE_WAIT count is $CLOSE_WAIT_COUNT (threshold: $THRESHOLD)"
    
    # 프로세스별 통계
    echo "Top processes with CLOSE_WAIT:"
    sudo ss -tanp | grep CLOSE_WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn | head -5
    
    # 슬랙 알림 전송
    curl -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"CLOSE_WAIT alert: $CLOSE_WAIT_COUNT connections\"}" \
        https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi
# Python 모니터링 스크립트
import subprocess
import time

def count_close_wait():
    result = subprocess.run(['netstat', '-an'], capture_output=True, text=True)
    count = result.stdout.count('CLOSE_WAIT')
    return count

def monitor_close_wait(threshold=50, interval=60):
    """CLOSE_WAIT 소켓을 주기적으로 모니터링"""
    while True:
        count = count_close_wait()
        print(f"CLOSE_WAIT count: {count}")
        
        if count > threshold:
            # 경고 로그
            print(f"⚠️  WARNING: CLOSE_WAIT count exceeded threshold ({count} > {threshold})")
            
            # 상세 정보 수집
            result = subprocess.run(['ss', '-tanp'], capture_output=True, text=True)
            with open('/var/log/close_wait_debug.log', 'a') as f:
                f.write(f"\n=== {time.ctime()} ===\n")
                f.write(result.stdout)
        
        time.sleep(interval)

if __name__ == '__main__':
    monitor_close_wait(threshold=50, interval=60)

CLOSE_WAIT 근본 원인 분석:

CLOSE_WAIT가 쌓이는 3가지 근본 원인:

1. 코드 버그
   - try-finally 누락
   - Exception 처리 미흡
   - 조기 return에서 close() 누락

2. 아키텍처 문제
   - 장시간 실행되는 백그라운드 작업
   - 연결을 전역 변수에 저장 후 관리 실패
   - 멀티스레딩/비동기에서 소켓 생명주기 관리 실패

3. 라이브러리 버그
   - 서드파티 라이브러리의 소켓 정리 버그
   - 오래된 버전 사용
   
해결 원칙:
→ 모든 소켓 open에 대응하는 close 보장
→ Context Manager 활용
→ 리소스 누수 테스트 자동화

9. CLOSING

양이 동시에 FIN을 전송한 경우 (드물게 발생).
sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: ESTABLISHED
    
    C->>S: FIN
    Note over C: FIN_WAIT_1
    
    S->>C: FIN (동시 전송)
    Note over S: FIN_WAIT_1
    
    Note over C: CLOSING
    Note over S: CLOSING
    
    C->>S: ACK
    S->>C: ACK
    
    Note over C: TIME_WAIT
    Note over S: TIME_WAIT

10. LAST_ACK

수동적 종료에서 FIN을 전송하고 최종 ACK를 기다리는 상태.
# 서버가 수동적으로 종료
# 1. 클라이언트가 FIN 전송 → 서버는 CLOSE_WAIT
# 2. 서버가 close() 호출 → FIN 전송, LAST_ACK 상태
# 3. 클라이언트의 ACK 수신 → CLOSED

11. TIME_WAIT - TCP의 가장 오해받는 상태

연결 종료 후 2MSL(Maximum Segment Lifetime) 동안 대기.
지연된 패킷 처리 및 포트 재사용 방지.

TIME_WAIT가 반드시 필요한 2가지 이유:

1. 마지막 ACK 손실 시나리오

정상적인 4-Way Handshake:
  Client              Server
    |                   |
    |  ------ FIN ----> |
    |  <----- ACK ----- |
    |  <----- FIN ----- |
    |  ------ ACK ----> |  ← 마지막 ACK
    |                   |
  TIME_WAIT           CLOSED
  (60초 대기)

만약 마지막 ACK가 손실된다면:
  Client              Server
    |                   |
    |  ------ ACK ----> | (손실!)
    |                   |
  TIME_WAIT           LAST_ACK
                      (재전송 타이머)
    |                   |
    |  <----- FIN ----- | ← FIN 재전송
    |  ------ ACK ----> | ← 재응답
    |                   |
  TIME_WAIT           CLOSED

TIME_WAIT가 없다면?
  → 클라이언트가 이미 CLOSED
  → FIN 재전송에 RST로 응답
  → 서버가 비정상 종료로 인식
  → 애플리케이션 로그에 에러 발생

2. 지연된 패킷 문제

시나리오: 빠른 연결-종료-재연결

연결 A:
  Client:12345 ←→ Server:80
  - 데이터 전송 중 일부 패킷이 네트워크에서 지연
  - 연결 종료
  
즉시 재연결 (TIME_WAIT 없이):
  Client:12345 ←→ Server:80 (동일한 4-튜플!)
  
문제:
  - 이전 연결 A의 지연된 패킷이 도착
  - 커널이 새 연결 B의 패킷으로 오인
  - 데이터 손상 발생!

TIME_WAIT 해결책:
  - 60초 동안 같은 4-튜플 사용 금지
  - MSL(보통 30초) * 2 = 60초
  - 네트워크의 모든 패킷이 소멸될 때까지 대기
  - 60초 후에만 포트 재사용 가능

2MSL (Maximum Segment Lifetime) 계산:

MSL = 네트워크에서 패킷이 살 수 있는 최대 시간
     = IP TTL * 홉 시간
     = 보통 30초 (RFC 1122 권장)

2MSL = 60초
  = 송신 패킷 소멸 시간 (30초)
  + 응답 패킷 소멸 시간 (30초)

Linux 기본값:
  net.ipv4.tcp_fin_timeout = 60 (초)
  
조정 가능하지만 권장하지 않음:
  sudo sysctl -w net.ipv4.tcp_fin_timeout=30
  # 위험: 지연된 패킷 문제 발생 가능

TIME_WAIT 문제 (포트 고갈)

# 문제 재현 코드
import socket
import time

# 클라이언트가 짧은 연결을 반복 생성
for i in range(65000):  # 포트 범위 초과 시도
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('server.com', 80))
    sock.send(b'GET / HTTP/1.1\r\n\r\n')
    sock.recv(1024)
    sock.close()  # 능동적 종료 → TIME_WAIT
    # 이 소켓은 60초 동안 포트 점유
    print(f"Connection {i} closed")

# 에러 발생:
# OSError: [Errno 99] Cannot assign requested address
# 원인: 사용 가능한 포트(32768~60999) 모두 TIME_WAIT 상태
# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l

# 많은 경우 (수천 개):
# - 클라이언트가 짧은 연결을 반복적으로 생성
# - 로드 밸런서, 프록시에서 흔함
# - HTTP/1.0 (Connection: close)

# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768  60999
# → 약 28,000개 포트만 사용 가능

TIME_WAIT 최적화 전략:

# 1. SO_REUSEADDR (서버 측)
# - TIME_WAIT 소켓이 있어도 바인딩 허용
# - 서버 재시작 시 "Address already in use" 방지
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
# 이전 서버의 TIME_WAIT 소켓이 있어도 바인딩 성공
# 2. SO_LINGER (위험! 신중히 사용)
import socket
import struct

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# linger = (onoff, linger_time)
# onoff=1, linger_time=0: close() 시 즉시 RST 전송 (TIME_WAIT 건너뜀)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('server.com', 80))
sock.send(b'data')
sock.close()  # RST 전송, TIME_WAIT 없음

# ⚠️ 위험:
# - 데이터 손실 가능 (송신 버퍼에 남은 데이터 버림)
# - 서버가 "Connection reset by peer" 에러 발생
# - 오직 로컬 테스트용으로만 사용
# 3. net.ipv4.tcp_tw_reuse (클라이언트 측)
# - 새로운 아웃바운드 연결 시 TIME_WAIT 소켓 재사용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# 안전한 이유:
# - 타임스탬프 옵션으로 이전 패킷 구분
# - 클라이언트에서만 작동 (서버는 영향 없음)
# 4. net.ipv4.tcp_tw_recycle (금지!)
# - Linux 4.12에서 제거됨
# - NAT 환경에서 심각한 문제 발생
sudo sysctl -w net.ipv4.tcp_tw_recycle=1  # ❌ 사용 금지!

# 문제:
# - 같은 NAT 뒤의 여러 클라이언트가 같은 IP로 보임
# - 타임스탬프 역전 시 패킷 드롭
# - 무작위 연결 실패

TIME_WAIT 근본 해결책: Keep-Alive

# ❌ 나쁜 예: 매번 새 연결 (TIME_WAIT 누적)
for i in range(1000):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('server.com', 80))
    sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\n\r\n')
    sock.recv(1024)
    sock.close()  # TIME_WAIT

# ✅ 좋은 예: 연결 재사용 (HTTP/1.1 Keep-Alive)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('server.com', 80))
for i in range(1000):
    sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\nConnection: keep-alive\r\n\r\n')
    sock.recv(1024)
# 한 번만 close() → TIME_WAIT 1개만
sock.close()
# HTTP/2, HTTP/3: 멀티플렉싱
# - 하나의 TCP 연결로 여러 요청 처리
# - TIME_WAIT 문제 거의 없음

언제 TIME_WAIT가 문제가 되나?

문제 되는 경우:
1. 로드 밸런서 / 리버스 프록시
   - 백엔드 서버로 짧은 연결 반복 생성
   - 포트 고갈 위험

2. 크롤러 / 스크래퍼
   - 수천 개 사이트에 연결
   - 빠른 속도로 연결 생성/종료

3. 부하 테스트 도구
   - 초당 수만 개 요청
   - 클라이언트 포트 고갈

문제 안 되는 경우:
1. 일반 웹 서버 (서버는 TIME_WAIT 거의 없음)
   - 클라이언트가 능동 종료
   - 서버는 CLOSE_WAIT → CLOSED

2. Keep-Alive 사용하는 애플리케이션
   - 연결 재사용
   - TIME_WAIT 최소화

3. 서버 측 (listen 소켓)
   - LISTEN 소켓은 TIME_WAIT 없음
   - Accept된 소켓만 영향받음

5. 상태 전이 다이어그램

완전한 TCP 상태 전이

stateDiagram-v2
    [*] --> CLOSED
    
    CLOSED --> LISTEN: passive open\n(server)
    CLOSED --> SYN_SENT: active open\n(client)
    
    LISTEN --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
    
    SYN_SENT --> ESTABLISHED: recv SYN-ACK\nsend ACK
    SYN_SENT --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
    
    SYN_RECEIVED --> ESTABLISHED: recv ACK
    
    ESTABLISHED --> FIN_WAIT_1: close()\nsend FIN
    ESTABLISHED --> CLOSE_WAIT: recv FIN\nsend ACK
    
    FIN_WAIT_1 --> FIN_WAIT_2: recv ACK
    FIN_WAIT_1 --> CLOSING: recv FIN\nsend ACK
    
    FIN_WAIT_2 --> TIME_WAIT: recv FIN\nsend ACK
    
    CLOSE_WAIT --> LAST_ACK: close()\nsend FIN
    
    CLOSING --> TIME_WAIT: recv ACK
    
    LAST_ACK --> CLOSED: recv ACK
    
    TIME_WAIT --> CLOSED: 2MSL timeout
    
    CLOSED --> [*]

클라이언트 vs 서버 상태 흐름

flowchart TB
    subgraph Client["클라이언트 (능동적 종료)"]
        C1[CLOSED] --> C2[SYN_SENT]
        C2 --> C3[ESTABLISHED]
        C3 --> C4[FIN_WAIT_1]
        C4 --> C5[FIN_WAIT_2]
        C5 --> C6[TIME_WAIT]
        C6 --> C7[CLOSED]
    end
    
    subgraph Server["서버 (수동적 종료)"]
        S1[CLOSED] --> S2[LISTEN]
        S2 --> S3[SYN_RECEIVED]
        S3 --> S4[ESTABLISHED]
        S4 --> S5[CLOSE_WAIT]
        S5 --> S6[LAST_ACK]
        S6 --> S7[CLOSED]
    end

6. netstat/ss로 상태 확인

netstat 명령어

기본 사용법

# 모든 TCP 연결 확인
netstat -an | grep tcp
# 특정 상태만 필터링
netstat -an | grep ESTABLISHED
netstat -an | grep TIME_WAIT
netstat -an | grep CLOSE_WAIT
# 프로세스 정보 포함 (Linux)
sudo netstat -anp | grep :80
# 통계 정보
netstat -s | grep -i tcp

상태별 개수 확인

# Linux/Mac
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 출력 예시:
#   150 ESTABLISHED
#    50 TIME_WAIT
#     5 CLOSE_WAIT
#     3 LISTEN

ss 명령어 (더 빠름)

# 모든 TCP 소켓
ss -tan
# ESTABLISHED 상태만
ss -tan state established
# TIME_WAIT 상태만
ss -tan state time-wait
# 프로세스 정보 포함
ss -tanp
# 통계
ss -s

ss 필터 예시

# 특정 포트
ss -tan '( dport = :80 or sport = :80 )'
# 특정 IP
ss -tan dst 192.168.1.100
# 여러 상태 조합
ss -tan state established state syn-sent
# 수신 큐와 송신 큐 크기 확인
ss -tan | awk '{print $2, $3}'

Windows PowerShell

# TCP 연결 확인
Get-NetTCPConnection
# ESTABLISHED 상태만
Get-NetTCPConnection -State Established
# 특정 포트
Get-NetTCPConnection -LocalPort 80
# 상태별 개수
Get-NetTCPConnection | Group-Object -Property State | Select-Object Name, Count

7. 실전 디버깅 시나리오

시나리오 1: TIME_WAIT 과다 (포트 고갈)

문제 증상

$ netstat -an | grep TIME_WAIT | wc -l
5000
# 새 연결 시도 시 에러
# "Cannot assign requested address"

원인

# ❌ 잘못된 코드: 짧은 연결 반복 생성
for i in range(10000):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('api.example.com', 80))
    sock.send(b'GET / HTTP/1.1\r\n\r\n')
    sock.recv(1024)
    sock.close()  # TIME_WAIT 상태로 전환
    # 60초 동안 포트 재사용 불가!

해결 방법

# ✅ 해결 1: 연결 재사용 (Keep-Alive)
import requests
session = requests.Session()
for i in range(10000):
    response = session.get('http://api.example.com/')
    # 같은 연결 재사용, TIME_WAIT 발생 안 함
# ✅ 해결 2: SO_REUSEADDR 설정
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# ✅ 해결 3: 커널 파라미터 조정 (Linux)
# /etc/sysctl.conf
# net.ipv4.tcp_tw_reuse = 1
# net.ipv4.tcp_fin_timeout = 30

시나리오 2: CLOSE_WAIT 누적 (리소스 누수)

문제 증상

$ netstat -an | grep CLOSE_WAIT | wc -l
1000
# 시간이 지나도 줄어들지 않음
# 결국 "Too many open files" 에러

원인

# ❌ 잘못된 코드: 소켓을 닫지 않음
def handle_client(sock):
    data = sock.recv(1024)
    process(data)
    # sock.close() 누락!
    # 클라이언트가 연결을 닫으면 CLOSE_WAIT 상태로 남음
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)
while True:
    client, addr = server.accept()
    handle_client(client)  # 소켓 누수!

해결 방법

# ✅ 해결 1: try-finally로 확실히 닫기
def handle_client(sock):
    try:
        data = sock.recv(1024)
        process(data)
    finally:
        sock.close()  # 반드시 실행
# ✅ 해결 2: Context Manager 사용
def handle_client(sock):
    with sock:
        data = sock.recv(1024)
        process(data)
    # 자동으로 close() 호출
# ✅ 해결 3: 타임아웃 설정
sock.settimeout(30)  # 30초 후 자동 종료

디버깅

# CLOSE_WAIT 소켓을 가진 프로세스 찾기
lsof -i -n | grep CLOSE_WAIT
# 프로세스별 CLOSE_WAIT 개수
lsof -i -n | grep CLOSE_WAIT | awk '{print $2}' | sort | uniq -c
# 특정 프로세스의 소켓 상태
lsof -p <PID> | grep TCP

시나리오 3: SYN_SENT 타임아웃

문제 증상

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
    sock.connect(('unreachable-server.com', 80))
except socket.timeout:
    print("❌ Connection timeout (SYN_SENT)")

원인 및 해결

# 1. 방화벽 확인
sudo iptables -L -n | grep 80
# 2. 라우팅 확인
traceroute unreachable-server.com
# 3. 서버 상태 확인
ping unreachable-server.com
# 4. 포트 스캔
nmap -p 80 unreachable-server.com

시나리오 4: 대량 ESTABLISHED 연결

문제 증상

$ netstat -an | grep ESTABLISHED | wc -l
10000
# 서버 응답 느려짐
# CPU, 메모리 사용량 증가

원인

# ❌ Keep-Alive로 연결을 무한정 유지
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# 유휴 연결이 계속 ESTABLISHED 상태로 남음

해결 방법

# ✅ 해결 1: 타임아웃 설정
sock.settimeout(60)  # 60초 유휴 시 자동 종료
# ✅ 해결 2: Keep-Alive 파라미터 조정
import socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)    # 60초 후 첫 probe
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)   # 10초 간격
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)      # 3번 시도
# ✅ 해결 3: 연결 풀 크기 제한
from concurrent.futures import ThreadPoolExecutor
max_connections = 1000
executor = ThreadPoolExecutor(max_workers=max_connections)

8. 성능 튜닝

Port Exhaustion (포트 고갈) 문제

클라이언트 포트 고갈 메커니즘:

TCP 연결 4-Tuple:
(Client IP, Client Port, Server IP, Server Port)

서버:
- 고정 포트 (예: 80, 443)
- 여러 클라이언트 IP 수용

클라이언트:
- 동적 포트 (Ephemeral Port)
- 범위: 32768 ~ 60999 (Linux)
- 총 개수: 28232개

예시: 클라이언트가 API 서버에 대량 요청

for i in range(100000):
    sock = requests.get('https://api.example.com')
    sock.close()

연결 과정:
1. 새 연결 생성:
   Client (192.168.1.100:32768) → Server (1.2.3.4:443)
   
2. 요청 완료, close() 호출:
   FIN 전송 → TIME_WAIT (60초)
   
3. 다음 연결:
   Client (192.168.1.100:32769) → Server (1.2.3.4:443)
   
4. 포트 증가...
   32770, 32771, ... 60999
   
5. 포트 고갈:
   28232개 모두 TIME_WAIT 상태
   → 새 연결 불가!

증상:
socket.error: [Errno 99] Cannot assign requested address
또는
socket.error: [Errno 24] Too many open files

TIME_WAIT 소켓 확인:
$ netstat -an | grep TIME_WAIT | wc -l
25000  ← 포트 거의 고갈

4-Tuple 유니크 제약:
(Client IP, Client Port, Server IP, Server Port)

같은 서버 (1.2.3.4:443)에 연결 시:
- Client IP: 고정 (192.168.1.100)
- Client Port: 28232개로 제한
- Server IP:Port: 고정 (1.2.3.4:443)
→ 최대 28232개 동시 연결

해결책:
1. TIME_WAIT 재사용 활성화
2. 포트 범위 확장
3. 연결 풀 사용
4. HTTP Keep-Alive

해결 방법 상세:

1. TIME_WAIT 재사용 (net.ipv4.tcp_tw_reuse):

기본 동작:
- TIME_WAIT 소켓은 60초간 포트 점유
- 새 연결은 사용 불가

tcp_tw_reuse=1:
- 타임스탬프 기반 재사용 허용
- 조건:
  * 새 연결의 타임스탬프 > 이전 연결
  * 클라이언트 측에서만 안전
  * 서버에서는 사용 금지 (들어오는 연결 혼란)

설정:
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

효과:
- TIME_WAIT 소켓을 1초 후 재사용 가능
- 포트 고갈 문제 해결
- 클라이언트 벤치마크/크롤러에 유용

주의:
- NAT 환경에서 타임스탬프 꼬일 수 있음
- 방화벽이 타임스탬프 제거하면 효과 없음

2. 포트 범위 확장:

기본:
net.ipv4.ip_local_port_range = 32768 60999
총 28232개

확장:
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"
총 55536개 (거의 2배)

효과:
- 동시 연결 수 증가
- TIME_WAIT 대기 시간 동안 더 많은 포트 사용 가능

3. SO_LINGER 옵션 (주의 필요):

sock.setsockopt(
  socket.SOL_SOCKET, 
  socket.SO_LINGER, 
  struct.pack('ii', 1, 0)
)

효과:
- close() 시 FIN 대신 RST 전송
- TIME_WAIT 우회 → 즉시 포트 재사용

부작용:
- 비정상 종료로 간주
- 버퍼에 남은 데이터 손실
- 상대방 "Connection reset by peer" 에러
→ 실무에서 비권장

4. 연결 풀 (Connection Pool):

문제:
for i in range(10000):
    r = requests.get(url)  # 매번 새 연결
    
TIME_WAIT 대량 발생

해결:
session = requests.Session()  # 연결 풀 생성
for i in range(10000):
    r = session.get(url)  # 연결 재사용
    
HTTP Keep-Alive:
Connection: keep-alive
→ 같은 소켓으로 여러 요청
→ TIME_WAIT 최소화

예시:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
adapter = HTTPAdapter(
    pool_connections=10,    # 호스트당 연결 풀 크기
    pool_maxsize=20,        # 최대 연결 수
    max_retries=Retry(total=3)
)
session.mount('https://', adapter)

for i in range(10000):
    r = session.get('https://api.example.com')
    
결과:
- 연결 재사용
- TIME_WAIT 10~20개로 제한
- 포트 고갈 문제 해결

5. 여러 IP 사용 (Multi-Homing):

4-Tuple 제약:
(Client IP, Client Port, Server IP, Server Port)

Client IP 추가:
eth0: 192.168.1.100  → 28232개 포트
eth0:0: 192.168.1.101  → 28232개 포트
eth0:1: 192.168.1.102  → 28232개 포트

총: 84696개 동시 연결 가능

코드:
sock = socket.socket()
sock.bind(('192.168.1.101', 0))  # 특정 IP 바인딩
sock.connect(('1.2.3.4', 443))

6. 서버 측 대응 (역방향 문제):

클라이언트가 서버를 공격:
- 대량 연결 생성 후 즉시 종료
- 서버에 TIME_WAIT 대량 발생
- 서버 포트는 고정이므로 직접 영향 없음
- 하지만 메모리/소켓 디스크립터 고갈

해결:
- 클라이언트 IP당 연결 수 제한
- Rate Limiting
- DDoS 방어 시스템

포트 고갈 모니터링:

# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l

# 특정 서버로의 TIME_WAIT
netstat -an | grep '1.2.3.4:443' | grep TIME_WAIT | wc -l

# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range

# 현재 사용 중인 포트
ss -tan | awk '{print $4}' | grep -o ':[0-9]*$' | sort | uniq | wc -l

# 실시간 모니터링
watch -n 1 'ss -tan state time-wait | wc -l'

# 포트별 상태 분포
ss -tan | awk '{print $1}' | sort | uniq -c

Linux 커널 파라미터

# /etc/sysctl.conf

# TIME_WAIT 재사용 허용 (클라이언트 측)
net.ipv4.tcp_tw_reuse = 1

# TIME_WAIT 재활용 (주의: NAT 환경에서 문제, 비활성화 권장)
net.ipv4.tcp_tw_recycle = 0

# 포트 범위 확장 (클라이언트)
net.ipv4.ip_local_port_range = 10000 65535

# FIN_WAIT_2 타임아웃 (기본 60초)
net.ipv4.tcp_fin_timeout = 30

# SYN 백로그 큐 크기
net.ipv4.tcp_max_syn_backlog = 8192

# 최대 연결 수 (서버 listen backlog)
net.core.somaxconn = 65535

# Keep-Alive 설정
net.ipv4.tcp_keepalive_time = 60     # 60초 후 첫 probe
net.ipv4.tcp_keepalive_intvl = 10    # 10초 간격
net.ipv4.tcp_keepalive_probes = 3    # 3번 시도

# 최대 파일 디스크립터 수
fs.file-max = 2097152

# 적용
sudo sysctl -p

SO_LINGER 옵션

import socket
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_LINGER 설정
# l_onoff=1, l_linger=0: close() 시 RST 전송 (TIME_WAIT 회피)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('example.com', 80))
sock.send(b'data')
# close() 호출 시:
# - 정상: FIN 전송 → TIME_WAIT
# - SO_LINGER(0): RST 전송 → 즉시 CLOSED
sock.close()
# ⚠️ 주의: RST는 비정상 종료로 간주됨
# 상대방이 "Connection reset by peer" 에러 발생 가능

연결 풀 패턴

import queue
import socket
import threading
class ConnectionPool:
    def __init__(self, host, port, pool_size=10):
        self.host = host
        self.port = port
        self.pool = queue.Queue(maxsize=pool_size)
        
        # 미리 연결 생성
        for _ in range(pool_size):
            sock = self._create_connection()
            self.pool.put(sock)
    
    def _create_connection(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.host, self.port))
        return sock
    
    def get_connection(self, timeout=5):
        """연결 가져오기"""
        try:
            return self.pool.get(timeout=timeout)
        except queue.Empty:
            return self._create_connection()
    
    def return_connection(self, sock):
        """연결 반환"""
        try:
            self.pool.put_nowait(sock)
        except queue.Full:
            sock.close()
    
    def close_all(self):
        """모든 연결 종료"""
        while not self.pool.empty():
            try:
                sock = self.pool.get_nowait()
                sock.close()
            except queue.Empty:
                break
# 사용
pool = ConnectionPool('api.example.com', 80, pool_size=20)
def make_request():
    sock = pool.get_connection()
    try:
        sock.send(b'GET / HTTP/1.1\r\n\r\n')
        response = sock.recv(4096)
        return response
    finally:
        pool.return_connection(sock)  # 재사용
# 여러 스레드에서 사용
for _ in range(1000):
    threading.Thread(target=make_request).start()
# TIME_WAIT 발생 안 함 (연결 재사용)

고급 주제

TCP Half-Open 연결

# Half-Open: 한쪽은 ESTABLISHED, 다른 쪽은 CLOSED
# 발생 원인: 네트워크 단절, 프로세스 강제 종료
# 감지 방법: Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Keep-Alive probe 전송 → 응답 없으면 연결 종료

TCP Reset (RST)

# RST 패킷이 전송되는 경우:
# 1. 존재하지 않는 포트로 연결 시도
# 2. SO_LINGER(0)로 close() 호출
# 3. 애플리케이션 강제 종료
# 4. 방화벽이 연결 차단
# RST 수신 시:
# - 즉시 CLOSED 상태로 전환
# - "Connection reset by peer" 에러

TCP Simultaneous Open

sequenceDiagram
    participant A as Host A
    participant B as Host B
    
    Note over A: CLOSED
    Note over B: CLOSED
    
    A->>B: SYN
    Note over A: SYN_SENT
    
    B->>A: SYN (동시 전송)
    Note over B: SYN_SENT
    
    Note over A: SYN_RECEIVED
    Note over B: SYN_RECEIVED
    
    A->>B: SYN-ACK
    B->>A: SYN-ACK
    
    Note over A: ESTABLISHED
    Note over B: ESTABLISHED

실전 모니터링 스크립트

Python 모니터링 도구

#!/usr/bin/env python3
import subprocess
import time
from collections import Counter
def get_tcp_states():
    """TCP 상태별 개수 조회"""
    try:
        # ss 명령어 사용 (더 빠름)
        result = subprocess.run(
            ['ss', '-tan'],
            capture_output=True,
            text=True
        )
        
        lines = result.stdout.strip().split('\n')[1:]  # 헤더 제외
        states = []
        
        for line in lines:
            parts = line.split()
            if len(parts) > 0:
                state = parts[0]
                states.append(state)
        
        return Counter(states)
    
    except FileNotFoundError:
        # ss가 없으면 netstat 사용
        result = subprocess.run(
            ['netstat', '-an'],
            capture_output=True,
            text=True
        )
        
        states = []
        for line in result.stdout.split('\n'):
            if 'tcp' in line.lower():
                parts = line.split()
                if len(parts) >= 6:
                    states.append(parts[5])
        
        return Counter(states)
def monitor_tcp_states(interval=5):
    """TCP 상태 실시간 모니터링"""
    print("🔍 TCP 연결 상태 모니터링 시작...\n")
    
    try:
        while True:
            states = get_tcp_states()
            
            print(f"\n{'='*50}")
            print(f"⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}")
            print(f"{'='*50}")
            
            for state, count in sorted(states.items()):
                bar = '█' * min(count, 50)
                print(f"{state:15s} {count:5d} {bar}")
            
            total = sum(states.values())
            print(f"{'─'*50}")
            print(f"{'TOTAL':15s} {total:5d}")
            
            # 경고
            if states.get('CLOSE_WAIT', 0) > 100:
                print("\n⚠️  WARNING: Too many CLOSE_WAIT connections!")
                print("   → Check if application is closing sockets properly")
            
            if states.get('TIME_WAIT', 0) > 5000:
                print("\n⚠️  WARNING: Too many TIME_WAIT connections!")
                print("   → Consider using connection pooling")
            
            time.sleep(interval)
    
    except KeyboardInterrupt:
        print("\n\n✅ 모니터링 종료")
if __name__ == '__main__':
    monitor_tcp_states(interval=5)

Bash 원라이너 모음

# 1. 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c | sort -rn
# 2. 특정 포트의 연결 수
netstat -an | grep ':80 ' | wc -l
# 3. IP별 연결 수 (상위 10개)
netstat -an | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# 4. TIME_WAIT 비율
echo "scale=2; $(netstat -an | grep TIME_WAIT | wc -l) / $(netstat -an | grep tcp | wc -l) * 100" | bc
# 5. 연결 수 실시간 모니터링
watch -n 1 'netstat -an | grep tcp | awk "{print \$6}" | sort | uniq -c'

프로그래밍 언어별 상태 확인

Python

import socket
import psutil
# 현재 프로세스의 연결 상태
connections = psutil.net_connections(kind='tcp')
for conn in connections:
    if conn.status == 'ESTABLISHED':
        print(f"{conn.laddr.ip}:{conn.laddr.port} -> {conn.raddr.ip}:{conn.raddr.port}")
        print(f"  Status: {conn.status}")
        print(f"  PID: {conn.pid}")
# 상태별 개수
from collections import Counter
states = Counter(conn.status for conn in connections)
print(states)

Node.js

const { exec } = require('child_process');
function getTCPStates(callback) {
  exec('netstat -an | grep tcp', (error, stdout) => {
    if (error) {
      callback(error);
      return;
    }
    
    const lines = stdout.trim().split('\n');
    const states = {};
    
    lines.forEach(line => {
      const parts = line.split(/\s+/);
      const state = parts[5];
      states[state] = (states[state] || 0) + 1;
    });
    
    callback(null, states);
  });
}
// 사용
getTCPStates((err, states) => {
  if (!err) {
    console.log('TCP States:', states);
  }
});

Go

package main
import (
    "fmt"
    "github.com/shirou/gopsutil/v3/net"
)
func main() {
    // TCP 연결 조회
    connections, err := net.Connections("tcp")
    if err != nil {
        panic(err)
    }
    
    // 상태별 개수
    states := make(map[string]int)
    for _, conn := range connections {
        states[conn.Status]++
    }
    
    // 출력
    for state, count := range states {
        fmt.Printf("%s: %d\n", state, count)
    }
}

실전 팁

1. 로드 밸런서 설정

# Nginx에서 Keep-Alive 설정
upstream backend {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
    
    # Keep-Alive 연결 유지
    keepalive 32;
}
server {
    location / {
        proxy_pass http://backend;
        
        # Keep-Alive 헤더
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # 타임아웃
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}

2. 데이터베이스 연결 풀

import psycopg2
from psycopg2 import pool
# 연결 풀 생성
connection_pool = psycopg2.pool.ThreadedConnectionPool(
    minconn=5,      # 최소 연결 수
    maxconn=20,     # 최대 연결 수
    host='localhost',
    database='mydb',
    user='user',
    password='pass'
)
def query_database(sql):
    conn = connection_pool.getconn()
    try:
        cursor = conn.cursor()
        cursor.execute(sql)
        result = cursor.fetchall()
        return result
    finally:
        connection_pool.putconn(conn)  # 연결 반환
# TIME_WAIT 발생 안 함 (연결 재사용)

3. HTTP 클라이언트 최적화

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
# 세션 재사용 + 재시도 전략
session = requests.Session()
retry_strategy = Retry(
    total=3,
    backoff_factor=1,
    status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(
    max_retries=retry_strategy,
    pool_connections=10,  # 연결 풀 크기
    pool_maxsize=20
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 사용
for i in range(1000):
    response = session.get('http://api.example.com/data')
    # 연결 재사용, TIME_WAIT 최소화

문제 해결 체크리스트

TIME_WAIT 과다

  • HTTP Keep-Alive 사용 확인
  • 연결 풀 구현 확인
  • net.ipv4.tcp_tw_reuse 활성화
  • net.ipv4.tcp_fin_timeout 감소 (30초)
  • 클라이언트 포트 범위 확대
# 클라이언트 포트 범위 확대
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"

CLOSE_WAIT 누적

  • 애플리케이션 코드에서 close() 호출 확인
  • try-finally 또는 with 문 사용
  • 타임아웃 설정 확인
  • 예외 처리 확인
  • 프로세스 재시작 (임시 해결)
# CLOSE_WAIT 디버깅
import traceback
def handle_connection(sock):
    try:
        data = sock.recv(1024)
        process(data)
    except Exception as e:
        print(f"❌ Error: {e}")
        traceback.print_exc()
    finally:
        print("🔒 Closing socket")
        sock.close()  # 반드시 실행

ESTABLISHED 과다

  • 유휴 연결 타임아웃 설정
  • Keep-Alive 파라미터 조정
  • 최대 연결 수 제한
  • 연결 풀 크기 조정
  • 로드 밸런서 설정 확인

정리

TCP 상태 요약

flowchart TD
    Start[연결 시작] --> Handshake[3-Way Handshake]
    Handshake --> Est["ESTABLISHED\n데이터 전송"]
    Est --> Close[연결 종료]
    Close --> Active{누가 먼저\n종료?}
    
    Active -->|클라이언트| TimeWait["TIME_WAIT\n60초 대기"]
    Active -->|서버| CloseWait["CLOSE_WAIT\nclose 호출 필요"]
    
    TimeWait --> End[CLOSED]
    CloseWait --> LastAck[LAST_ACK]
    LastAck --> End

핵심 포인트

상태주의사항해결 방법
TIME_WAIT포트 고갈 가능연결 재사용, Keep-Alive
CLOSE_WAIT소켓 누수반드시 close() 호출
SYN_SENT연결 타임아웃타임아웃 설정, 재시도
ESTABLISHED과다 연결연결 풀, 타임아웃

디버깅 명령어 요약

# 상태 확인
netstat -an | grep tcp
ss -tan
# 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 프로세스별 연결
lsof -i -n -P | grep TCP
# 실시간 모니터링
watch -n 1 'ss -s'
# 특정 포트
netstat -an | grep ':80'
ss -tan '( sport = :80 or dport = :80 )'

성능 최적화 요약

# ✅ 연결 재사용
session = requests.Session()
# ✅ 연결 풀
pool = ConnectionPool(size=20)
# ✅ 타임아웃 설정
sock.settimeout(30)
# ✅ Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# ✅ 리소스 정리
try:
    # 작업
    pass
finally:
    sock.close()

관련 글 (내부 링크)

TCP와 함께 보면 좋은 네트워크 관련 가이드입니다:



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드. S… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

TCP, 네트워크, netstat, ESTABLISHED, TIME_WAIT, CLOSE_WAIT, 소켓프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.