안동민 개발노트 아이콘

안동민 개발노트

6장 : TCP

TCP의 핵심 특성

지금까지 물리적 신호 전송부터 IP 기반 라우팅까지, 패킷이 네트워크를 횡단하여 목적지 컴퓨터에 도달하는 과정을 살펴보았습니다. 하지만 패킷이 컴퓨터에 도착하는 것만으로는 충분하지 않습니다.

하나의 컴퓨터에서 웹 브라우저, 이메일 클라이언트, 메신저가 동시에 실행되고 있다면, 도착한 패킷을 어떤 프로그램에 전달해야 하는지 결정해야 합니다. 또한 중간에 패킷이 손실되거나 순서가 뒤바뀌는 상황에도 대처해야 합니다.

이 역할을 담당하는 계층이 전송 계층(Transport Layer)이며, 대표적인 프로토콜이 TCP(Transmission Control Protocol)입니다. TCP는 IP 위에서 동작하면서 애플리케이션에게 연결 지향의 신뢰성 있는 바이트 스트림을 제공합니다.


연결 지향, 신뢰성, 순서 보장, 바이트 스트림

TCP를 정의하는 네 가지 핵심 특성이 있습니다.

연결 지향(Connection-Oriented): TCP는 데이터를 주고받기 전에 연결 상태를 만듭니다. 3-way handshake를 통해 양쪽의 초기 순서 번호와 옵션을 맞추고, 이후 세그먼트는 이 연결 상태를 기준으로 해석됩니다. 이 연결은 논리적인 것이며, 회선 교환처럼 물리적 경로를 독점하는 것은 아닙니다.

신뢰성(Reliability): TCP는 ACK, 재전송, 체크섬, 순서 번호를 이용해 애플리케이션에게 손실과 중복을 숨긴 순서 있는 바이트 스트림을 제공하려고 합니다. 다만 네트워크나 상대 호스트가 실패하면 데이터가 반드시 도착하는 것은 아니며, 결국 연결 오류로 드러날 수 있습니다.

순서 보장(Ordered Delivery): IP 패킷은 손실되거나, 중복되거나, 순서가 바뀌어 도착할 수 있습니다. TCP는 각 바이트에 순서 번호(Sequence Number)를 부여하고, 수신 측이 애플리케이션에 넘기기 전에 원래 순서로 재조립합니다.

바이트 스트림(Byte Stream): TCP는 데이터를 연속된 바이트의 흐름으로 취급합니다. 메시지의 경계를 구분하지 않습니다. 1000바이트를 한 번에 보내든, 100바이트씩 10번 나눠 보내든, 수신 측에서는 단순히 1000바이트의 연속된 스트림을 받습니다. 메시지 경계가 필요하면 애플리케이션이 직접 처리해야 합니다.


TCP 세그먼트 구조

TCP 데이터 전송의 단위를 세그먼트(Segment)라고 합니다. TCP 헤더의 구조를 살펴봅시다.

필드크기설명
Source Port16비트출발지 포트 번호
Destination Port16비트목적지 포트 번호
Sequence Number32비트이 세그먼트의 첫 데이터 바이트 순서 번호
Acknowledgment Number32비트ACK 플래그가 설정됐을 때 다음에 받기를 기대하는 바이트 번호
Data Offset4비트TCP 헤더 길이 (4바이트 단위)
Control Bits8비트CWR, ECE, URG, ACK, PSH, RST, SYN, FIN
Window Size16비트수신 측이 더 받을 수 있는 바이트 수 (흐름 제어)
Checksum16비트pseudo header, TCP 헤더, 데이터에 대한 오류 검출
Urgent Pointer16비트URG 플래그 사용 시 긴급 데이터 위치
Options가변MSS, Window Scale, Timestamp, SACK Permitted 등

핵심 플래그들의 의미를 정리합니다.

플래그의미사용 시점
SYN연결 개시, 순서 번호 동기화3-way handshake
ACK확인 응답, ACK 번호 유효연결 수립 후 대부분의 세그먼트
FIN더 보낼 데이터가 없음을 알리는 정상 종료연결 종료
RST연결 강제 리셋비정상 종료, 거부, 상태 불일치
PSH수신 TCP가 데이터를 지체 없이 넘기도록 요청대화형 통신에서 보일 수 있음
URG긴급 포인터 유효과거 Telnet 등, 현대에는 드묾
ECE/CWRECN 기반 혼잡 신호ECN을 사용하는 연결

포트 번호의 역할

전송 계층이 어떤 프로그램에 데이터를 전달할지 결정하는 데 사용하는 것이 포트 번호(Port Number)입니다.

IP 주소가 특정 호스트의 네트워크 인터페이스를 찾는 데 쓰인다면, 포트 번호는 그 호스트 안에서 어떤 애플리케이션 서비스 또는 소켓으로 전달할지를 정하는 데 쓰입니다. IP 주소가 건물의 주소라면, 포트 번호는 건물 안의 호실 번호와 비슷합니다.

포트 번호는 16비트 정수로 0~65535 범위를 가지며, 세 그룹으로 나뉩니다.

구분범위설명예시
System / Well-known0~1023표준 서비스용으로 관리되는 낮은 포트HTTP(80), HTTPS(443), SSH(22), DNS(53)
User / Registered1024~49151애플리케이션 서비스 등록에 쓰이는 범위MySQL(3306), PostgreSQL(5432), Redis(6379)
Dynamic / Private49152~65535임시 포트로 쓰도록 예약된 범위클라이언트 출발지 포트

많은 Unix 계열 시스템에서는 1024 미만 포트에 바인드하려면 관리자 권한이 필요하지만, 이것은 운영체제 정책입니다. 또한 실제 임시 포트 범위는 OS 설정에 따라 IANA의 Dynamic/Private 범위와 다를 수 있습니다.


소켓이란 무엇인가

네트워크 프로그래밍에서 자주 등장하는 소켓(Socket)의 정확한 정의를 짚어 두겠습니다.

소켓은 운영체제가 애플리케이션에 제공하는 통신 끝점입니다. 로컬 끝점은 보통 프로토콜 + 로컬 IP + 로컬 포트로 표현하고, 하나의 TCP 연결은 여기에 원격 IP + 원격 포트까지 더한 4-tuple로 식별합니다.

tcp_socket_example.py
import socket

# === TCP 서버 소켓 ===
def start_server():
    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))
    server.listen(5)  # accept 대기 큐 크기 힌트

    print(f"서버 소켓: {server.getsockname()}")
    # 출력: 서버 소켓: ('0.0.0.0', 8080)

    while True:
        client, addr = server.accept()
        print(f"클라이언트 연결: {addr}")
        # 출력: 클라이언트 연결: ('192.168.1.10', 50234)

        # TCP 연결을 식별하는 양 끝점 확인
        print(f"  로컬:  {client.getsockname()}")  # ('10.0.0.1', 8080)
        print(f"  원격:  {client.getpeername()}")   # ('192.168.1.10', 50234)

        data = client.recv(1024)
        client.sendall(b"Hello from server")
        client.close()

# === TCP 클라이언트 소켓 ===
def connect_to_server():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(("10.0.0.1", 8080))

    print(f"로컬 소켓:  {client.getsockname()}")
    # 출력: ('192.168.1.10', 50234) ← OS가 임시 포트 자동 할당
    print(f"원격 소켓:  {client.getpeername()}")
    # 출력: ('10.0.0.1', 8080)

    client.sendall(b"Hello from client")
    response = client.recv(1024)
    print(f"응답: {response.decode()}")
    client.close()

실무에서 포트 관련 명령

port_check.sh
#!/bin/bash
# === 포트 사용 현황 확인 ===

echo "=== 열려 있는 TCP 포트 목록 ==="
# Linux
ss -tlnp
# 또는
netstat -tlnp

echo ""
echo "=== 특정 포트 사용 프로세스 확인 ==="
# 8080 포트를 사용하는 프로세스
ss -tlnp | grep :8080
# lsof -i :8080

echo ""
echo "=== TCP 연결 상태별 개수 ==="
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
# 출력 예시:
#  1523 ESTAB         ← 활성 연결
#   342 TIME-WAIT     ← 종료 대기
#    15 CLOSE-WAIT    ← 상대방의 FIN 수신 후 대기
#     5 LISTEN        ← 연결 대기 중인 서버 소켓

echo ""
echo "=== ESTABLISHED 연결 상세 ==="
ss -tnp state established

echo ""
echo "=== Windows 포트 확인 ==="
# netstat -ano | findstr :8080
# tasklist /FI "PID eq 12345"
상태의미해석
LISTEN연결 대기 중 (서버)정상
ESTABLISHED활성 연결정상
TIME_WAIT연결 종료 후 지연 세그먼트 대기정상 상태, 급증하면 연결 패턴 확인
CLOSE_WAIT상대방 FIN 수신, 내 FIN 미전송지속 누적이면 close 누락 의심
SYN_SENTSYN 전송, 응답 대기대량이면 서버 도달성 문제 의심
SYN_RECVSYN 수신, SYN-ACK 전송대량이면 backlog 압박이나 SYN Flood 의심

다음 절에서는 TCP 연결이 실제로 어떻게 수립되고 종료되는지, 3-way handshake4-way handshake를 살펴보겠습니다.