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 연결 상태 개요
  2. 연결 수립: 3-Way Handshake
  3. 연결 종료: 4-Way Handshake
  4. 11가지 TCP 상태 상세 설명
  5. 상태 전이 다이어그램
  6. netstat/ss로 상태 확인
  7. 실전 디버깅 시나리오
  8. 성능 튜닝

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 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
    
    Server->>Client: 2. SYN-ACK (seq=200, ack=101)
    Note over Server: SYN_RECEIVED
    
    Client->>Server: 3. ACK (ack=201)
    Note over Client: ESTABLISHED
    Note over Server: ESTABLISHED
    
    Note over Client,Server: 데이터 전송 가능

상태 전이

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

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<br/>(능동적 종료)
    participant Server as Server<br/>(수동적 종료)
    
    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: 애플리케이션이<br/>close() 호출할 때까지 대기
    
    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<br/>큐가 가득 참]
    
    style Attacker fill:#ff6b6b
    style Void fill:#ff6b6b
# 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

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() 호출할 때까지<br/>FIN_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 문제 (소켓 리소스 누수):

# ❌ 잘못된 코드 (CLOSE_WAIT 누적)
def handle_request(sock):
    data = sock.recv(1024)
    process(data)
    # sock.close()를 호출하지 않음!
    # 클라이언트가 연결을 닫으면 CLOSE_WAIT 상태로 남음

# ✅ 올바른 코드
def handle_request(sock):
    try:
        data = sock.recv(1024)
        process(data)
    finally:
        sock.close()  # 반드시 닫기
# CLOSE_WAIT 상태 확인
netstat -an | grep CLOSE_WAIT

# CLOSE_WAIT가 많다면 애플리케이션 버그!
# 프로세스별 소켓 수 확인
lsof -p <PID> | grep TCP | wc -l

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

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

TIME_WAIT의 목적:

  1. 지연된 패킷 처리: 네트워크에 남아있는 패킷이 새 연결에 영향을 주지 않도록
  2. 안정적인 종료: 마지막 ACK가 손실되면 상대방이 FIN을 재전송할 수 있도록
import socket
import time

# 클라이언트가 연결을 닫으면
sock.close()

# 상태: TIME_WAIT (약 60초)
# 이 시간 동안 같은 (src_ip, src_port, dst_ip, dst_port) 튜플 재사용 불가

# 60초 후
# 상태: CLOSED

TIME_WAIT 문제 (포트 고갈):

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

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

5. 상태 전이 다이어그램

완전한 TCP 상태 전이

stateDiagram-v2
    [*] --> CLOSED
    
    CLOSED --> LISTEN: passive open<br/>(server)
    CLOSED --> SYN_SENT: active open<br/>(client)
    
    LISTEN --> SYN_RECEIVED: recv SYN<br/>send SYN-ACK
    
    SYN_SENT --> ESTABLISHED: recv SYN-ACK<br/>send ACK
    SYN_SENT --> SYN_RECEIVED: recv SYN<br/>send SYN-ACK
    
    SYN_RECEIVED --> ESTABLISHED: recv ACK
    
    ESTABLISHED --> FIN_WAIT_1: close()<br/>send FIN
    ESTABLISHED --> CLOSE_WAIT: recv FIN<br/>send ACK
    
    FIN_WAIT_1 --> FIN_WAIT_2: recv ACK
    FIN_WAIT_1 --> CLOSING: recv FIN<br/>send ACK
    
    FIN_WAIT_2 --> TIME_WAIT: recv FIN<br/>send ACK
    
    CLOSE_WAIT --> LAST_ACK: close()<br/>send 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. 성능 튜닝

Linux 커널 파라미터

# /etc/sysctl.conf

# TIME_WAIT 재사용 허용
net.ipv4.tcp_tw_reuse = 1

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

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

# 최대 연결 수
net.core.somaxconn = 65535

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

# Keep-Alive 설정
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 3

# 적용
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<br/>데이터 전송]
    Est --> Close[연결 종료]
    Close --> Active{누가 먼저<br/>종료?}
    
    Active -->|클라이언트| TimeWait[TIME_WAIT<br/>60초 대기]
    Active -->|서버| CloseWait[CLOSE_WAIT<br/>close 호출 필요]
    
    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 연결 상태를 이해하면 TIME_WAIT 포트 고갈, CLOSE_WAIT 소켓 누수 같은 네트워크 문제를 효과적으로 디버깅하고 해결할 수 있습니다.

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3