연결 수립과 종료
TCP 3-way handshake와 FIN 종료의 순서 번호·상태 전이를 추적하고 TIME_WAIT 및 RST 상황을 진단합니다.
TCP가 연결 지향이라는 말은, 데이터를 보내기 전에 양쪽 TCP가 순서 번호와 옵션을 맞추고 연결 상태를 만든다는 뜻입니다.
연결 수립은 보통 3-way handshake, 정상 종료는 양쪽 송신 방향을 각각 닫는 FIN 기반 종료로 설명합니다.
면접에서는 “SYN, ACK 순서”만 외우기 쉽지만, 실무 디버깅에서는 상태 전이, sequence/ack 번호, TIME_WAIT, RST의 의미까지 같이 봐야 합니다.
3-way Handshake
TCP 연결 수립은 양쪽이 서로의 초기 순서 번호(ISN)를 알리고 확인하는 과정입니다.
SYN은 client ISN을, SYN-ACK은 server ISN과 client 도달성을, ACK는 server 도달성을 확인한다.
- Client
SYN seq=x client ISN 제시
- Server
SYN-ACK seq=y ack=x+1 server ISN과 응답
- Client
ACK ack=y+1 server 응답 확인
- Both
ESTABLISHED 양쪽 상태 생성
1단계 — SYN: 클라이언트가 active open을 수행하며 SYN, seq=x를 보냅니다.
SYN은 순서 번호 공간을 1만큼 소비하므로, 상대가 기대하는 다음 번호는 x+1입니다.
2단계 — SYN-ACK: 서버는 클라이언트의 SYN을 확인하는 ACK x+1과 자신의 SYN, seq=y를 한 세그먼트에 담아 보냅니다.
이때 MSS, Window Scale, SACK Permitted, Timestamps 같은 TCP 옵션도 협상됩니다.
3단계 — ACK: 클라이언트는 서버의 SYN을 확인하는 ACK y+1을 보냅니다.
이 마지막 ACK가 서버에 도착하면 서버도 연결을 완료 상태로 보고, 이후 애플리케이션 데이터 전송이 안정적으로 시작됩니다.
일부 구현과 상황에서는 세 번째 ACK에 데이터를 실을 수 있지만, 서버가 연결을 완전히 받아들이는 시점과 애플리케이션 처리 정책은 구현에 따라 달라질 수 있습니다.
왜 3-way일까요?
양쪽이 “내 SYN이 상대에게 도착했고, 상대의 SYN도 내가 받았다”를 모두 확인해야 하기 때문입니다.
오래된 SYN이 나중에 도착하면 서버는 새 연결로 착각할 수 있다.
- Old SYN
네트워크 지연 이전 연결의 잔여 패킷
- Server
SYN만 보고 연결 생성 2-way라 확인 부족
- Client
미 종료된 상태 의도한 연결 아님
- 3-way
ACK로 최종 확인 지연 SYN 걸러냄
ISN의 역할
ISN(Initial Sequence Number)은 연결마다 새롭게 고르는 32비트 시작 순서 번호입니다.
단순히 0부터 시작하지 않는 이유는 이전 연결의 지연 세그먼트와 새 연결을 구분하고, sequence number 예측 공격을 어렵게 만들기 위해서입니다.
현대 구현은 보통 시간에 따라 변하는 값과 4-tuple, 비밀값 기반 함수를 조합해 공격자가 다음 ISN을 맞히기 어렵게 만듭니다.
sequence number와 TIME_WAIT는 이전 연결의 잔여 segment가 새 연결에 섞이지 않도록 시간을 벌어 준다.
- 연결 A
ISN 1000 segment가 지연됨
- 종료
TIME_WAIT 잔여 패킷 소멸 대기
- 연결 B
ISN 90000 번호 공간 분리
- old segment
낡은 번호 새 연결에서 폐기
TCP 연결은 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트로 이루어진 4-tuple로 식별됩니다.
같은 4-tuple이 재사용될 때 과거 세그먼트가 네트워크에 남아 있으면 새 연결과 혼동될 수 있습니다.
그래서 ISN 선택과 TIME_WAIT는 서로 보완적으로 동작합니다.
FIN 기반 정상 종료
TCP는 양방향 바이트 스트림입니다.
한쪽이 FIN을 보냈다는 것은 “나는 더 이상 보낼 데이터가 없다”는 뜻이지, 상대방도 즉시 보낼 데이터가 없다는 뜻은 아닙니다.
TCP 종료는 방향별로 닫힌다. 한쪽 FIN이 곧 양쪽 데이터 종료를 뜻하지 않는다.
- Client
FIN client → server 송신 종료
- Server
ACK client FIN 확인
- Server
FIN server → client 송신 종료
- Client
ACK TIME_WAIT 진입
일반적인 active close 흐름은 다음과 같습니다.
| 단계 | 세그먼트 | 상태 의미 |
|---|---|---|
| 1 | Client → Server: FIN | 클라이언트 송신 방향 종료, 클라이언트는 FIN-WAIT-1 |
| 2 | Server → Client: ACK | 서버는 FIN을 확인하고 CLOSE-WAIT, 클라이언트는 FIN-WAIT-2 |
| 3 | Server → Client: FIN | 서버 애플리케이션도 close한 뒤 자신의 송신 방향 종료 |
| 4 | Client → Server: ACK | 클라이언트는 마지막 FIN을 확인하고 TIME-WAIT로 이동 |
이 네 단계는 가장 흔한 설명용 흐름입니다.
실제로는 양쪽이 거의 동시에 FIN을 보내는 simultaneous close, FIN과 ACK가 같은 세그먼트에 실리는 경우, RST로 abort되는 경우도 있습니다.
중요한 원칙은 FIN이 한 방향 송신 스트림의 끝을 알리고, FIN도 SYN처럼 순서 번호 공간을 1 소비한다는 점입니다.
TCP 연결 상태 전이
TCP 상태명은 “무슨 패킷을 보냈는지”, “무슨 패킷을 기다리는지”, “애플리케이션이 close를 호출했는지”를 보여 줍니다.
상태가 쌓이면 마지막 패킷과 다음에 기다리는 이벤트를 먼저 연결한다.
- SYN-SENT
SYN 보냄 SYN-ACK 대기
- SYN-RECEIVED
SYN-ACK 보냄 최종 ACK 대기
- ESTABLISHED
데이터 가능 정상 연결
- CLOSE-WAIT
상대 FIN 받음 앱 close 대기
- TIME-WAIT
마지막 ACK 보냄 지연 세그먼트 격리
운영에서 자주 보는 상태는 다음처럼 해석하면 됩니다.
| 상태 | 의미 | 오래 누적될 때 보는 지점 |
|---|---|---|
| SYN-SENT | SYN을 보내고 SYN-ACK을 기다림 | 서버 listen, 방화벽, 라우팅, SYN 손실 |
| SYN-RECEIVED | SYN을 받고 SYN-ACK을 보낸 뒤 최종 ACK 대기 | SYN backlog, SYN flood, handshake 미완료 |
| ESTABLISHED | 데이터 송수신 가능 | 정상 연결 또는 장기 연결 |
| CLOSE-WAIT | 상대 FIN은 받았고, 로컬 애플리케이션 close 대기 | 애플리케이션이 소켓을 닫지 않는 문제 가능 |
| FIN-WAIT-2 | 내 FIN은 ACK 됐고, 상대 FIN을 기다림 | 상대 애플리케이션 종료 지연 또는 정책 timeout |
| TIME-WAIT | 마지막 ACK 이후 지연 세그먼트 격리 | 짧은 연결이 매우 많을 때 포트/테이블 압박 점검 |
TIME_WAIT 상태
TIME_WAIT는 오류가 아니라 정상 종료를 안전하게 마무리하기 위한 상태입니다.
보통 마지막 ACK를 보낸 쪽, 즉 active closer가 TIME_WAIT에 들어갑니다.
동시 종료 같은 예외에서는 양쪽이 TIME_WAIT에 들어갈 수도 있습니다.
마지막 ACK가 유실되면 상대는 FIN을 다시 보낼 수 있고, TIME_WAIT 소켓이 그 FIN에 다시 ACK할 수 있다.
- 1 마지막 ACK
마지막 ACK active closer가 보냄 상대 FIN 확인
- 2 ACK 유실 가능
ACK 유실 가능 상대는 FIN 재전송 종료 확인 부족
- 3 TIME_WAIT
TIME_WAIT 2MSL 대기 재전송 FIN 처리
- 4 종료
종료 잔여 segment 소멸 tuple 재사용 가능
TIME_WAIT의 목적은 두 가지입니다.
첫째, 마지막 ACK가 손실되면 상대가 FIN을 재전송할 수 있으므로, TIME_WAIT 쪽이 그 FIN에 다시 ACK할 수 있어야 합니다.
둘째, 같은 4-tuple을 너무 빨리 재사용했을 때 과거 연결의 지연 세그먼트가 새 연결에 섞이는 일을 줄입니다.
RFC 문맥에서는 2MSL 대기가 등장하지만, 운영체제별 실제 TIME_WAIT 지속 시간과 튜닝 가능 여부는 다릅니다.
Linux, Windows, BSD 계열의 기본값과 정책이 다를 수 있으므로 고정된 초 단위 숫자로 외우기보다 목적과 구현 차이를 같이 봐야 합니다.
TIME_WAIT 실무 문제와 완화
짧은 TCP 연결을 매우 많이 만들면 TIME_WAIT가 누적되어 임시 포트, 연결 추적 테이블, 로드밸런서 상태 테이블을 압박할 수 있습니다.
하지만 TIME_WAIT 자체를 없애는 것보다 연결 재사용과 트래픽 구조를 먼저 보는 편이 안전합니다.
# Linux: TCP 상태별 개수
ss -tan | awk 'NR > 1 { count[$1]++ } END { for (s in count) print count[s], s }' | sort -rn
# Linux: TIME-WAIT만 확인
ss -tan state time-wait | wc -l
# Linux: 임시 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 특정 목적지로 열린 연결이 많은지 확인
ss -tan '( dport = :443 or sport = :443 )'| 접근 | 효과 | 주의사항 |
|---|---|---|
| Connection Pool | 연결 생성과 종료 자체를 줄임 | DB, HTTP 클라이언트에서 가장 먼저 검토 |
| Keep-Alive / HTTP/2 / HTTP/3 | 요청 여러 개를 같은 연결 또는 세션에 태움 | 서버 idle timeout, 프록시 정책과 함께 조정 |
| 임시 포트 범위 확대 | 클라이언트 쪽 포트 고갈 여유 증가 | 연결 남발의 근본 해결은 아님 |
| 커널 파라미터 조정 | 특정 OS에서 재사용 조건을 완화할 수 있음 | OS 버전별 의미가 달라 문서 확인 후 적용 |
| SO_LINGER(0) / RST 종료 | 정상 FIN 종료를 건너뛰고 즉시 abort | 데이터 손실과 프로토콜 오류 위험, 일반 권장 아님 |
비정상 종료: RST
FIN은 정상 종료이고, RST(Reset)는 연결 상태를 즉시 폐기하라는 신호입니다.
수신 측은 RST가 현재 연결 문맥에서 유효한지 확인한 뒤 연결을 abort합니다.
RST가 발생하는 대표적인 경우는 다음과 같습니다.
- 리슨하지 않는 포트로 SYN이 도착한 경우: 호스트가 연결을 거부하며 RST로 응답할 수 있습니다.
- 한쪽 TCP 상태가 사라졌는데 상대가 데이터를 보내는 경우: half-open 상태를 발견하고 RST로 정리할 수 있습니다.
- 애플리케이션이나 OS가 abortive close를 수행한 경우: 예를 들어
SO_LINGER(0)은 FIN 대신 RST를 유도할 수 있습니다. - 방화벽이나 프록시가 연결을 거부하는 경우: 장비 정책에 따라 RST를 보내거나, 아무 응답 없이 drop할 수 있습니다.
RST는 빠르지만 정상적인 데이터 배출을 보장하지 않습니다.
따라서 애플리케이션이 “보낼 데이터가 모두 상대에게 전달되었다”는 의미로 RST를 사용하면 안 됩니다.
연결 수립/종료 실습
import socket
import subprocess
import sys
import time
PORT = 8765
def show_connection_state(description):
print(f"\n--- {description} ---")
if sys.platform == "win32":
command = ["netstat", "-an"]
else:
command = ["ss", "-tan"]
result = subprocess.run(command, capture_output=True, text=True, check=False)
for line in result.stdout.splitlines():
if str(PORT) in line:
print(" ", line.strip())
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", PORT))
server.listen(1)
show_connection_state("서버 LISTEN")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", PORT))
conn, _ = server.accept()
show_connection_state("ESTABLISHED")
client.sendall(b"Hello TCP!")
print("\n수신 데이터:", conn.recv(1024).decode())
client.close()
time.sleep(0.1)
show_connection_state("클라이언트 close 이후")
conn.close()
server.close()
time.sleep(0.1)
show_connection_state("종료 이후")연결 관련 면접 질문 정리
| 질문 | 핵심 답변 |
|---|---|
| 3-way handshake를 설명하세요 | SYN → SYN-ACK → ACK, 양쪽 ISN과 도달 가능성을 확인 |
| 왜 2-way가 아니라 3-way인가요? | 서버가 보낸 SYN이 클라이언트에게 도달했는지 확인해야 함 |
| 4-way 종료를 설명하세요 | FIN → ACK → FIN → ACK 형태로 양방향 송신 스트림을 닫음 |
| 왜 종료는 분리되나요? | 한쪽 FIN 이후에도 상대는 남은 데이터를 보낼 수 있음 |
| TIME_WAIT의 목적은? | 마지막 ACK 손실 대비와 지연 세그먼트 격리 |
| CLOSE_WAIT이 누적되면? | 애플리케이션이 close를 호출하지 않는 문제 가능 |
| RST와 FIN의 차이는? | FIN은 정상 종료, RST는 연결 상태 즉시 폐기 |
| SYN flood란? | 대량 SYN으로 반연결 상태 자원을 고갈시키는 공격 |
연결 수립·종료 문제는 SYN, SYN-ACK, ACK, FIN, RST가 끊긴 구간부터 좁혀 갑니다.
수립, 사용, 종료 중 어디서 멈췄는지 먼저 나누면 원인이 빨리 좁혀진다.
- 수립
SYN → SYN-ACK → ACK SYN-SENT, SYN-RECEIVED가 쌓이면 경로와 listen을 본다.
- 사용
ESTABLISHED RTT, 재전송, zero window는 처리 지연 신호다.
- 정상 종료
FIN → ACK → FIN → ACK CLOSE-WAIT은 앱 close, FIN-WAIT-2는 상대 종료를 본다.
- 비정상 종료
RST 또는 drop 거부, abort, 중간 장비 정책과 직전 패킷을 확인한다.
다음 절에서는 TCP가 데이터를 안정적으로 전송하기 위한 핵심 메커니즘인 흐름 제어와 혼잡 제어를 살펴보겠습니다.