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바이트의 연속된 스트림을 받습니다.
메시지 경계가 필요하면 애플리케이션이 직접 처리해야 합니다.
동일한 여섯 바이트의 서로 다른 쓰기·세그먼트·읽기 경계
두 write의 바이트를 순서대로 이어 보냅니다. 수신 앱은 AB·CDEF로 읽을 수도 있습니다. 각 단계의 값과 전환 이유를 비교합니다.
TCP · 바이트 스트림
바이트 순서는 같아도 쓰기·세그먼트·읽기 경계는 달라진다
TCP는 ABCDEF의 순서를 전달합니다. 메시지 길이나 구분자는 애플리케이션이 정해야 하며, 한 번 쓴 데이터가 한 번의 read로 반환된다는 보장은 없습니다.
가능한 분할의 한 예입니다. read 호출 크기·반환 횟수는 고정되지 않으며 메시지 경계는 애플리케이션이 정의합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 앱 쓰기부터 앱 읽기까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 동일한 여섯 바이트의 서로 다른 쓰기·세그먼트·읽기 경계 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 앱 쓰기 두 write의 바이트를 순서대로 이어 보냅니다. |
|
|
| 세그먼트 가능한 전송 분할 ABCD·EF를 비교합니다. |
|
|
| 앱 읽기 수신 앱은 AB·CDEF로 읽을 수도 있습니다. |
|
|
동일한 여섯 바이트의 서로 다른 쓰기·세그먼트·읽기 경계
두 write의 바이트를 순서대로 이어 보냅니다. 수신 앱은 AB·CDEF로 읽을 수도 있습니다. 각 단계의 값과 전환 이유를 비교합니다.
TCP · 바이트 스트림
바이트 순서는 같아도 쓰기·세그먼트·읽기 경계는 달라진다
TCP는 ABCDEF의 순서를 전달합니다. 메시지 길이나 구분자는 애플리케이션이 정해야 하며, 한 번 쓴 데이터가 한 번의 read로 반환된다는 보장은 없습니다.
가능한 분할의 한 예입니다. read 호출 크기·반환 횟수는 고정되지 않으며 메시지 경계는 애플리케이션이 정의합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 앱 쓰기부터 앱 읽기까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 동일한 여섯 바이트의 서로 다른 쓰기·세그먼트·읽기 경계 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 앱 쓰기 두 write의 바이트를 순서대로 이어 보냅니다. |
|
|
| 세그먼트 가능한 전송 분할 ABCD·EF를 비교합니다. |
|
|
| 앱 읽기 수신 앱은 AB·CDEF로 읽을 수도 있습니다. |
|
|
경계를 복원하는 애플리케이션: 한 메시지를 ASCII 바이트와 마지막 개행 하나로 표현한다고 정합시다. 메시지 안에는 개행을 넣지 않는 이 연습 프로토콜에서 ABC\nDEF\n을 보냈더라도 수신 함수가 AB, C\nD, EF\n으로 나누어 반환할 수 있습니다. TCP 바이트 순서는 유지됐고, 받은 조각을 모아 개행을 찾아야 두 메시지가 복원됩니다.
# recv 대신 미리 정한 조각을 처리합니다. 소켓이나 네트워크를 사용하지 않습니다.
chunks = [b"AB", b"C\nD", b"EF\n"]
pending = b""
messages = []
for chunk in chunks:
pending += chunk
while b"\n" in pending:
message, pending = pending.split(b"\n", 1)
messages.append(message)
print(messages) # [b'ABC', b'DEF']
print(pending) # b''독립 복원: 마지막 조각만 b"EF"로 바꾸면 완성된 메시지와 남는 버퍼는 각각 무엇일까요? 아직 상대가 연결을 닫았다는 관측은 없습니다. recv() 횟수만으로 메시지 둘이 모두 완성됐다고 말할 수 있는지도 설명하세요.
구분자와 남은 바이트 확인
완성된 메시지는 ABC 하나이고 pending에는 DEF 세 바이트가 남습니다. 두 번째 개행이 아직 없으므로 이 프로토콜의 두 번째 메시지를 완성 처리하지 않습니다. 더 받은 바이트를 이어 붙여야 하며, 나중에 EOF가 오면 이 프로토콜의 불완전 메시지 처리 규칙을 적용해야 합니다. EOF나 읽기 횟수 자체가 빠진 구분자를 대신하지 않습니다. 실제 서비스에서는 최대 메시지·버퍼 크기와 읽기 제한 시간, 구분자 이스케이프 또는 길이 헤더도 정해야 합니다.
Python 공식 Socket HOWTO의 부분 송수신과 메시지 경계 설명을 함께 읽습니다. sendall()의 성공이나 TCP ACK는 상대 업무 로직의 저장·처리 완료를 뜻하지 않으므로, 업무 성공 확인은 애플리케이션 응답 규칙으로 정합니다.
TCP 세그먼트 구조
TCP 데이터 전송의 단위를 세그먼트(Segment)라고 합니다.
아래는 RFC 9293의 기본 TCP 헤더입니다. 추가 확장에 따른 비트 의미는 해당 규격에서 확인합니다.
| 필드 | 크기 · 설명 |
|---|---|
| Source Port | 크기: 16비트 설명: 출발지 포트 번호 |
| Destination Port | 크기: 16비트 설명: 목적지 포트 번호 |
| Sequence Number | 크기: 32비트 설명: SYN이 없을 때 첫 데이터 바이트 번호; SYN이면 ISN |
| Acknowledgment Number | 크기: 32비트 설명: ACK 플래그가 설정됐을 때 다음에 받기를 기대하는 바이트 번호 |
| Data Offset | 크기: 4비트 설명: TCP 헤더 길이 (4바이트 단위) |
| Control Bits | 크기: 8비트 설명: CWR, ECE, URG, ACK, PSH, RST, SYN, FIN |
| Window Size | 크기: 16비트 설명: 수신 윈도우 필드; 협상한 Window Scale을 함께 적용 |
| Checksum | 크기: 16비트 설명: pseudo header, TCP 헤더, 데이터에 대한 오류 검출 |
| Urgent Pointer | 크기: 16비트 설명: URG 플래그 사용 시 긴급 데이터 위치 |
| Options | 크기: 가변 설명: MSS, Window Scale, Timestamp, SACK Permitted 등 |
핵심 플래그들의 의미를 정리합니다.
| 플래그 | 의미 · 사용 시점 |
|---|---|
| SYN | 의미: 연결 개시, 순서 번호 동기화 사용 시점: 3-way handshake |
| ACK | 의미: 확인 응답, ACK 번호 유효 사용 시점: 연결 수립 후 대부분의 세그먼트 |
| FIN | 의미: 더 보낼 데이터가 없음을 알리는 정상 종료 사용 시점: 연결 종료 |
| RST | 의미: 연결 강제 리셋 사용 시점: 비정상 종료, 거부, 상태 불일치 |
| PSH | 의미: 수신 TCP가 데이터를 지체 없이 넘기도록 요청 사용 시점: 대화형 통신에서 보일 수 있음 |
| URG | 의미: 긴급 포인터 유효 사용 시점: 과거 Telnet 등, 현대에는 드묾 |
| ECE/CWR | 의미: ECN 기반 혼잡 신호 사용 시점: ECN을 사용하는 연결 |
포트 번호의 역할
전송 계층이 어떤 프로그램에 데이터를 전달할지 결정하는 데 사용하는 것이 포트 번호(Port Number)입니다.
IP 주소가 특정 호스트의 네트워크 인터페이스를 찾는 데 쓰인다면, 포트 번호는 그 호스트 안에서 어떤 애플리케이션 서비스 또는 소켓으로 전달할지를 정하는 데 쓰입니다.
IP 주소가 건물의 주소라면, 포트 번호는 건물 안의 호실 번호와 비슷합니다.
다만 목적지 포트 하나만으로 항상 수신 대상이 결정되는 것은 아닙니다.
운영체제는 먼저 TCP/UDP 같은 프로토콜을 나누고, 로컬 IP와 로컬 포트로 후보 소켓을 찾습니다.
TCP의 이미 수립된 연결은 여기에 원격 IP와 원격 포트까지 더한 4-tuple로 식별되며, 새 연결 요청은 LISTEN 소켓에서 accept()를 거쳐 별도의 연결 소켓이 됩니다.
포트 번호는 16비트 정수로 0~65535 범위를 가지며, 세 그룹으로 나뉩니다.
-
System / Well-known: 범위: 0~1023; 설명: 표준 서비스용으로 관리되는 낮은 포트; 예시: HTTP(80), HTTPS(443), SSH(22), DNS(53).
-
User / Registered: 범위: 1024~49151; 설명: 애플리케이션 서비스 등록에 쓰이는 범위; 예시: MySQL(3306), PostgreSQL(5432), Redis(6379).
-
Dynamic / Private: 범위: 49152~65535; 설명: 임시 포트로 쓰도록 예약된 범위; 예시: 클라이언트 출발지 포트.
많은 Unix 계열 시스템에서는 1024 미만 포트에 바인드하려면 관리자 권한이 필요하지만, 이것은 운영체제 정책입니다.
또한 실제 임시 포트 범위는 OS 설정에 따라 IANA의 Dynamic/Private 범위와 다를 수 있습니다.
소켓이란 무엇인가
네트워크 프로그래밍에서 자주 등장하는 소켓(Socket)의 정확한 정의를 짚어 두겠습니다.
소켓은 운영체제가 애플리케이션에 제공하는 통신 끝점입니다.
로컬 끝점은 보통 프로토콜 + 로컬 IP + 로컬 포트로 표현하고, 하나의 TCP 연결은 여기에 원격 IP + 원격 포트까지 더한 4-tuple로 식별합니다.
이 예제는 함수 정의입니다. 서버와 클라이언트 함수를 별도 실행 흐름에서 호출해야 하며, 주석의 IP·포트는 출력 예시입니다. recv() 한 번이 전체 메시지를 돌려준다는 보장은 없으므로 실제 프로토콜에서는 메시지 길이나 구분자에 따른 수신 처리가 필요합니다.
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()실무에서 포트 관련 명령
#!/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_SENT | 의미: SYN 전송, 응답 대기 해석: 대량이면 서버 도달성 문제 의심 |
| SYN_RECV | 의미: SYN 수신, SYN-ACK 전송 해석: 대량이면 backlog 압박이나 SYN Flood 의심 |
TCP 상태를 확인할 때는 연결 대기인 LISTEN과 통신 중인 ESTABLISHED를 먼저 구분합니다.
다음 절에서는 TCP 연결이 실제로 어떻게 수립되고 종료되는지, 3-way handshake와 4-way handshake를 살펴보겠습니다.