TCP의 핵심 특성
지금까지 물리적 신호 전송부터 IP 기반 라우팅까지, 패킷이 네트워크를 횡단하여 목적지 컴퓨터에 도달하는 과정을 살펴보았습니다. 하지만 패킷이 컴퓨터에 도착하는 것만으로는 충분하지 않습니다.
하나의 컴퓨터에서 웹 브라우저, 이메일 클라이언트, 메신저가 동시에 실행되고 있다면, 도착한 패킷을 어떤 프로그램에 전달해야 하는지 결정해야 합니다. 또한 중간에 패킷이 손실되거나 순서가 뒤바뀌는 상황에도 대처해야 합니다.
이 모든 것을 담당하는 것이 전송 계층(Transport Layer)이며, 그 핵심 프로토콜이 TCP(Transmission Control Protocol)입니다.
┌─────────────────────────────────┐
│ Application Layer │ HTTP, FTP, SSH, DNS
│ (어떤 서비스를 사용할지) │
├─────────────────────────────────┤
│ Transport Layer ← 지금 여기 │ TCP, UDP
│ (어떤 프로세스에 전달할지) │ 포트 번호로 프로세스 식별
│ (신뢰성, 순서, 흐름 제어) │
├─────────────────────────────────┤
│ Network Layer │ IP
│ (어떤 컴퓨터로 보낼지) │
├─────────────────────────────────┤
│ Data Link / Physical │ Ethernet, Wi-Fi
│ (같은 네트워크에서 전달) │
└─────────────────────────────────┘연결 지향, 신뢰성, 순서 보장, 바이트 스트림
TCP를 정의하는 네 가지 핵심 특성이 있습니다.
┌──────────────────────────────────────────────────────────┐
│ TCP의 4대 보장 │
├────────────────┬─────────────────────────────────────────┤
│ 연결 지향 │ 데이터 전송 전 3-way handshake로 연결 │
│ (Connection) │ 양쪽이 준비되었음을 확인한 후 통신 시작 │
├────────────────┼─────────────────────────────────────────┤
│ 신뢰성 │ 패킷 손실 → 재전송, 손상 → 폐기+재요청 │
│ (Reliability) │ ACK/타임아웃 기반으로 전달 보장 │
├────────────────┼─────────────────────────────────────────┤
│ 순서 보장 │ Sequence Number로 원래 순서 재조립 │
│ (Ordering) │ 늦게 도착한 패킷도 올바른 위치에 배치 │
├────────────────┼─────────────────────────────────────────┤
│ 바이트 스트림 │ 메시지 경계 없이 연속된 바이트로 전달 │
│ (Byte Stream) │ 경계 구분은 애플리케이션의 책임 │
└────────────────┴─────────────────────────────────────────┘연결 지향(Connection-Oriented): TCP는 데이터를 주고받기 전에 반드시 연결을 수립합니다. 전화를 걸어 상대방이 받아야 대화를 시작하는 것처럼, TCP도 3-way handshake를 통해 연결을 먼저 설정합니다. 이 연결은 논리적인 것이며, 회선 교환처럼 물리적 경로를 독점하는 것은 아닙니다.
신뢰성(Reliability): TCP는 데이터가 반드시 도착하도록 보장합니다. 패킷이 손실되면 재전송하고, 손상되면 폐기 후 재요청합니다. 수신 측이 확인 응답(ACK)을 보내지 않으면 송신 측이 해당 데이터를 다시 보냅니다.
순서 보장(Ordered Delivery): 패킷 교환 방식에서는 각 패킷이 다른 경로를 통해 도착할 수 있으므로, 순서가 뒤바뀌는 것이 자연스럽습니다. TCP는 각 바이트에 순서 번호(Sequence Number)를 부여하여, 수신 측에서 원래 순서대로 재조립합니다.
바이트 스트림(Byte Stream): TCP는 데이터를 연속된 바이트의 흐름으로 취급합니다. 메시지의 경계를 구분하지 않습니다. 1000바이트를 한 번에 보내든, 100바이트씩 10번 나눠 보내든, 수신 측에서는 단순히 1000바이트의 연속된 스트림을 받습니다. 메시지 경계가 필요하면 애플리케이션이 직접 처리해야 합니다.
송신 측 (send 호출)
send("Hello") → 5바이트
send("World") → 5바이트
send("!") → 1바이트
수신 측 (recv 호출) — 아래 중 어느 것이든 가능
recv(11) → "HelloWorld!" ← 한 번에 수신
recv(6) → "HelloW" ← 절반만 수신
recv(3) → "Hel" ← 부분 수신
recv(8) → "loWorld!" ← 나머지 수신
→ TCP는 메시지 경계를 보장하지 않음!
→ HTTP: Content-Length나 Transfer-Encoding으로 경계 처리
→ WebSocket: 프레임 헤더에 길이 포함TCP 세그먼트 구조
TCP 데이터 전송의 단위를 세그먼트(Segment)라고 합니다. TCP 헤더의 구조를 살펴봅시다.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Source Port │ Destination Port │
├───────────────────────────────┼───────────────────────────────┤
│ Sequence Number │
├───────────────────────────────────────────────────────────────┤
│ Acknowledgment Number │
├───────┬───────┬─┬─┬─┬─┬─┬─┬───────────────────────────────────┤
│ Offset│Reserve│U│A│P│R│S│F│ Window Size │
│ (4bit)│ (3bit)│R│C│S│S│Y│I│ (16bit) │
│ │ │G│K│H│T│N│N│ │
├───────┴───────┴─┴─┴─┴─┴─┴─┼───────────────────────────────────┤
│ Checksum │ Urgent Pointer │
├─────────────────────────────┴─────────────────────────────────┤
│ Options (가변 길이) │
├───────────────────────────────────────────────────────────────┤
│ Data (페이로드) │
└───────────────────────────────────────────────────────────────┘| 필드 | 크기 | 설명 |
|---|---|---|
| Source Port | 16비트 | 출발지 포트 번호 |
| Destination Port | 16비트 | 목적지 포트 번호 |
| Sequence Number | 32비트 | 이 세그먼트의 첫 바이트 순서 번호 |
| Acknowledgment Number | 32비트 | 다음에 받기를 기대하는 바이트 번호 |
| Data Offset | 4비트 | TCP 헤더 길이 (4바이트 단위) |
| Flags | 6비트 | URG, ACK, PSH, RST, SYN, FIN |
| Window Size | 16비트 | 수신 가능한 바이트 수 (흐름 제어) |
| Checksum | 16비트 | 헤더+데이터 무결성 검증 |
| Urgent Pointer | 16비트 | 긴급 데이터 위치 (URG 플래그 사용 시) |
| Options | 가변 | MSS, Window Scale, Timestamp 등 |
핵심 플래그들의 의미를 정리합니다.
| 플래그 | 의미 | 사용 시점 |
|---|---|---|
| SYN | 연결 개시, 순서 번호 동기화 | 3-way handshake |
| ACK | 확인 응답, Ack 번호 유효 | 연결 수립 후 거의 모든 패킷 |
| FIN | 연결 종료 요청 | 4-way handshake |
| RST | 연결 강제 리셋 | 비정상 종료, 거부 |
| PSH | 버퍼링 없이 즉시 애플리케이션에 전달 | 대화형 통신 |
| URG | 긴급 데이터 존재 | Telnet 인터럽트 등 (거의 미사용) |
포트 번호의 역할
전송 계층이 어떤 프로그램에 데이터를 전달할지 결정하는 데 사용하는 것이 포트 번호(Port Number)입니다.
IP 주소가 특정 컴퓨터를 식별한다면, 포트 번호는 그 컴퓨터에서 실행 중인 특정 프로세스를 식별합니다. IP 주소가 건물의 주소라면, 포트 번호는 건물 안의 호실 번호와 같습니다.
서버 (93.184.216.34)
┌─────────────────────────────────────┐
│ │
│ :22 ← SSH 데몬 │
│ :80 ← Nginx (HTTP) │
│ :443 ← Nginx (HTTPS) │
│ :3306 ← MySQL │
│ :6379 ← Redis │
│ :8080 ← Spring Boot API │
│ │
│ 같은 IP 주소에 여러 서비스가 │
│ 포트 번호로 구분되어 동시 실행 │
└─────────────────────────────────────┘
클라이언트 요청
93.184.216.34:443 → HTTPS 서비스로 연결
93.184.216.34:22 → SSH 서비스로 연결포트 번호는 16비트 정수로 0~65535 범위를 가지며, 세 그룹으로 나뉩니다.
| 구분 | 범위 | 설명 | 예시 |
|---|---|---|---|
| Well-known | 0~1023 | 표준 서비스 예약 (관리자 권한 필요) | HTTP(80), HTTPS(443), SSH(22), DNS(53) |
| Registered | 1024~49151 | 특정 애플리케이션 등록 | MySQL(3306), PostgreSQL(5432), Redis(6379) |
| Dynamic/Ephemeral | 49152~65535 | OS가 자동 할당하는 임시 포트 | 클라이언트 출발지 포트 |
소켓이란 무엇인가
네트워크 프로그래밍에서 자주 등장하는 소켓(Socket)의 정확한 정의를 짚어 두겠습니다.
소켓은 통신의 끝점(Endpoint)을 식별하는 조합으로, IP 주소 + 포트 번호 + 프로토콜(TCP/UDP)로 구성됩니다.
하나의 TCP 연결 = 두 개의 소켓 쌍
클라이언트 소켓 서버 소켓
(192.168.1.10, 50000, TCP) ←→ (93.184.216.34, 443, TCP)
연결 식별자 = 4-tuple
(src IP, src Port, dst IP, dst Port)
(192.168.1.10, 50000, 93.184.216.34, 443)
같은 서버 포트에 여러 클라이언트 연결
Client A: (192.168.1.10, 50000) ←→ (93.184.216.34, 443)
Client B: (192.168.1.10, 50001) ←→ (93.184.216.34, 443)
Client C: (10.0.0.5, 49200) ←→ (93.184.216.34, 443)
→ 4-tuple이 모두 다르므로 각각 독립적인 연결
→ 서버의 443 포트 하나로 수만 개의 동시 연결 처리 가능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) # 백로그 큐 크기
print(f"서버 소켓: {server.getsockname()}")
# 출력: 서버 소켓: ('0.0.0.0', 8080)
while True:
client, addr = server.accept()
print(f"클라이언트 연결: {addr}")
# 출력: 클라이언트 연결: ('192.168.1.10', 50234)
# 4-tuple 확인
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()실무에서 포트 관련 명령
#!/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 미전송 | 누적되면 애플리케이션 버그 가능 |
| SYN_SENT | SYN 전송, 응답 대기 | 대량이면 서버 접속 불가 의심 |
| SYN_RECV | SYN 수신, SYN-ACK 전송 | 대량이면 SYN Flood 공격 의심 |
다음 절에서는 TCP 연결이 실제로 어떻게 수립되고 종료되는지, 3-way handshake와 4-way handshake를 살펴보겠습니다.