안동민 개발노트 아이콘

안동민 개발노트

6장 : TCP

연결 수립과 종료

TCP가 연결 지향이라는 말은, 데이터를 보내기 전에 양쪽 TCP가 순서 번호와 옵션을 맞추고 연결 상태를 만든다는 뜻입니다. 연결 수립은 보통 3-way handshake, 정상 종료는 양쪽 송신 방향을 각각 닫는 FIN 기반 종료로 설명합니다.

면접에서는 “SYN, ACK 순서”만 외우기 쉽지만, 실무 디버깅에서는 상태 전이, sequence/ack 번호, TIME_WAIT, RST의 의미까지 같이 봐야 합니다.


3-way Handshake

TCP 연결 수립은 양쪽이 서로의 초기 순서 번호(ISN)를 알리고 확인하는 과정입니다.

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도 내가 받았다”를 모두 확인해야 하기 때문입니다.


ISN의 역할

ISN(Initial Sequence Number)은 연결마다 새롭게 고르는 32비트 시작 순서 번호입니다. 단순히 0부터 시작하지 않는 이유는 이전 연결의 지연 세그먼트와 새 연결을 구분하고, sequence number 예측 공격을 어렵게 만들기 위해서입니다. 현대 구현은 보통 시간에 따라 변하는 값과 4-tuple, 비밀값 기반 함수를 조합해 공격자가 다음 ISN을 맞히기 어렵게 만듭니다.

TCP 연결은 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트로 이루어진 4-tuple로 식별됩니다. 같은 4-tuple이 재사용될 때 과거 세그먼트가 네트워크에 남아 있으면 새 연결과 혼동될 수 있습니다. 그래서 ISN 선택과 TIME_WAIT는 서로 보완적으로 동작합니다.


FIN 기반 정상 종료

TCP는 양방향 바이트 스트림입니다. 한쪽이 FIN을 보냈다는 것은 “나는 더 이상 보낼 데이터가 없다”는 뜻이지, 상대방도 즉시 보낼 데이터가 없다는 뜻은 아닙니다.

일반적인 active close 흐름은 다음과 같습니다.

단계세그먼트상태 의미
1Client → Server: FIN클라이언트 송신 방향 종료, 클라이언트는 FIN-WAIT-1
2Server → Client: ACK서버는 FIN을 확인하고 CLOSE-WAIT, 클라이언트는 FIN-WAIT-2
3Server → Client: FIN서버 애플리케이션도 close한 뒤 자신의 송신 방향 종료
4Client → Server: ACK클라이언트는 마지막 FIN을 확인하고 TIME-WAIT로 이동

이 네 단계는 가장 흔한 설명용 흐름입니다. 실제로는 양쪽이 거의 동시에 FIN을 보내는 simultaneous close, FIN과 ACK가 같은 세그먼트에 실리는 경우, RST로 abort되는 경우도 있습니다. 중요한 원칙은 FIN이 한 방향 송신 스트림의 끝을 알리고, FIN도 SYN처럼 순서 번호 공간을 1 소비한다는 점입니다.


TCP 연결 상태 전이

TCP 상태명은 “무슨 패킷을 보냈는지”, “무슨 패킷을 기다리는지”, “애플리케이션이 close를 호출했는지”를 보여 줍니다.

운영에서 자주 보는 상태는 다음처럼 해석하면 됩니다.

상태의미오래 누적될 때 보는 지점
SYN-SENTSYN을 보내고 SYN-ACK을 기다림서버 listen, 방화벽, 라우팅, SYN 손실
SYN-RECEIVEDSYN을 받고 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에 들어갈 수도 있습니다.

TIME_WAIT의 목적은 두 가지입니다. 첫째, 마지막 ACK가 손실되면 상대가 FIN을 재전송할 수 있으므로, TIME_WAIT 쪽이 그 FIN에 다시 ACK할 수 있어야 합니다. 둘째, 같은 4-tuple을 너무 빨리 재사용했을 때 과거 연결의 지연 세그먼트가 새 연결에 섞이는 일을 줄입니다.

RFC 문맥에서는 2MSL 대기가 등장하지만, 운영체제별 실제 TIME_WAIT 지속 시간과 튜닝 가능 여부는 다릅니다. Linux, Windows, BSD 계열의 기본값과 정책이 다를 수 있으므로 고정된 초 단위 숫자로 외우기보다 목적과 구현 차이를 같이 봐야 합니다.


TIME_WAIT 실무 문제와 완화

짧은 TCP 연결을 매우 많이 만들면 TIME_WAIT가 누적되어 임시 포트, 연결 추적 테이블, 로드밸런서 상태 테이블을 압박할 수 있습니다. 하지만 TIME_WAIT 자체를 없애는 것보다 연결 재사용과 트래픽 구조를 먼저 보는 편이 안전합니다.

time_wait_check.sh
# 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를 사용하면 안 됩니다.


연결 수립/종료 실습

connection_states.py
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으로 반연결 상태 자원을 고갈시키는 공격

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