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로 상태 확인
- 실전 디버깅 시나리오
목차
- TCP 연결 상태 개요
- 연결 수립: 3-Way Handshake
- 연결 종료: 4-Way Handshake
- 11가지 TCP 상태 상세 설명
- 상태 전이 다이어그램
- 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 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의 목적:
- 지연된 패킷 처리: 네트워크에 남아있는 패킷이 새 연결에 영향을 주지 않도록
- 안정적인 종료: 마지막 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 소켓 누수 같은 네트워크 문제를 효과적으로 디버깅하고 해결할 수 있습니다.