소켓과 전송 계층 (3) - 멀티플렉싱과 패킷 흐름 - soo:bak
작성일 :
멀티플렉싱의 역사
1870년대, 전화가 발명되었을 때 통화를 하려면 교환원에게 요청해야 했습니다.
교환원이 물리적으로 전선을 연결했습니다.
한 통화에 하나의 전용 회선이 필요했습니다.
이 방식에는 문제가 있습니다.
100명이 동시에 통화하려면 100개의 회선이 필요합니다.
회선 대부분은 대화 중 침묵(데이터 없음)이므로 낭비가 심합니다.
시분할 다중화(TDM, Time Division Multiplexing)는 이 문제를 해결했습니다.
여러 통화가 하나의 회선을 시간으로 나눠 씁니다.
각 통화는 짧은 시간 슬롯을 할당받고, 그 시간에만 회선을 사용합니다.
인간은 밀리초 단위의 끊김을 인지하지 못하므로 연속된 대화처럼 느껴집니다.
패킷 교환(Packet Switching)은 더 나아갔습니다.
데이터를 작은 패킷으로 나누고, 각 패킷이 독립적으로 네트워크를 지나갑니다.
전용 회선을 예약하지 않습니다.
보낼 데이터가 있을 때만 네트워크를 사용합니다.
인터넷은 패킷 교환 네트워크입니다.
하나의 물리적 연결(이더넷 케이블, Wi-Fi)로 수백 개의 동시 연결이 가능합니다.
이것을 멀티플렉싱(Multiplexing)이라고 합니다.
포트 번호: 프로세스 식별자
멀티플렉싱의 핵심 요소는 포트 번호(Port Number)입니다.
IP 주소가 컴퓨터를 식별한다면, 포트 번호는 그 컴퓨터의 프로세스를 식별합니다.
왜 16비트인가?
포트 번호는 16비트 정수입니다.
0 ~ 65535, 총 65536개의 값을 가질 수 있습니다.
왜 하필 16비트일까요?
32비트면 40억 개의 포트를 사용할 수 있지만, TCP/UDP 헤더가 커집니다.
8비트면 256개뿐이라 부족합니다.
16비트(65536개)는 현실적인 동시 연결 수를 감당하면서 헤더 크기를 작게 유지하는 절충안입니다.
포트 범위의 의미
1
2
3
0 ~ 1023: Well-known Ports (잘 알려진 포트)
1024 ~ 49151: Registered Ports (등록된 포트)
49152 ~ 65535: Dynamic/Ephemeral Ports (임시 포트)
Well-known Ports (0 ~ 1023)
표준 서비스가 사용하는 포트입니다.
IANA(Internet Assigned Numbers Authority)가 관리합니다.
1
2
3
4
5
22: SSH
25: SMTP
53: DNS
80: HTTP
443: HTTPS
Unix/Linux에서 이 범위의 포트에 바인딩하려면 root 권한이 필요합니다.
이것은 보안 조치입니다.
일반 사용자가 80번 포트에서 가짜 웹 서버를 실행하는 것을 방지합니다.
Registered Ports (1024 ~ 49151)
특정 애플리케이션이 사용하도록 등록된 포트입니다.
강제는 아니지만 관례적으로 따릅니다.
1
2
3
4
3306: MySQL
5432: PostgreSQL
6379: Redis
8080: HTTP 대체
Ephemeral Ports (49152 ~ 65535)
클라이언트가 아웃바운드 연결에 사용하는 임시 포트입니다.
운영체제가 자동으로 할당합니다.
브라우저가 웹 서버에 연결할 때 출발지 포트는 이 범위에서 할당됩니다.
운영체제의 패킷 라우팅
패킷이 네트워크 인터페이스에 도착하면 운영체제가 어느 소켓으로 전달할지 결정해야 합니다.
이 과정을 역다중화(Demultiplexing)라고 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
네트워크 인터페이스
│
↓ 패킷 수신
┌───────────────────────────────────────────────┐
│ 커널 │
│ │
│ IP 헤더 확인 → 목적지 IP가 내 것인가? │
│ ↓ Yes │
│ 프로토콜 확인 → TCP? UDP? │
│ ↓ TCP │
│ 5-tuple 검색 → 소켓 테이블에서 매칭 │
│ ↓ │
│ 소켓 수신 버퍼에 데이터 추가 │
│ │
└───────────────────────────────────────────────┘
│
↓ read() 호출 시
┌───────────────────────────────────────────────┐
│ 애플리케이션 │
└───────────────────────────────────────────────┘
소켓 테이블
커널은 모든 소켓을 추적하는 테이블을 유지합니다.
1
2
3
4
5
6
7
8
9
10
소켓 테이블 (단순화)
┌────────────────────────────────────────────────────────────┐
│ 프로토콜 │ 로컬IP │ 로컬포트 │ 원격IP │ 원격포트 │ 상태 │
├────────────────────────────────────────────────────────────┤
│ TCP │ 0.0.0.0 │ 80 │ * │ * │ LISTEN │
│ TCP │ 192.168.1.1 │ 80 │ 10.0.0.5 │ 52001 │ ESTABLISHED │
│ TCP │ 192.168.1.1 │ 80 │ 10.0.0.5 │ 52002 │ ESTABLISHED │
│ TCP │ 192.168.1.1 │ 80 │ 203.0.113.10 │ 49500 │ ESTABLISHED │
│ UDP │ 0.0.0.0 │ 53 │ * │ * │ - │
└────────────────────────────────────────────────────────────┘
패킷이 도착하면:
- IP 헤더에서 목적지 IP와 프로토콜(TCP/UDP) 확인
- TCP/UDP 헤더에서 목적지 포트와 출발지 포트 확인
- 5-tuple로 소켓 테이블 검색
- 매칭되는 소켓의 수신 버퍼에 데이터 추가
LISTEN 소켓의 특수성
LISTEN 상태의 소켓은 원격 주소가 *(와일드카드)입니다.
어떤 클라이언트의 SYN도 받을 수 있습니다.
SYN이 도착하면 새 연결 소켓이 생성되고, 그 소켓에 구체적인 원격 주소가 설정됩니다.
1
2
3
4
5
LISTEN 소켓: (TCP, 0.0.0.0, 80, *, *)
↓
SYN from 10.0.0.5:52001
↓
새 소켓 생성: (TCP, 192.168.1.1, 80, 10.0.0.5, 52001)
하나의 서버 포트, 수천 개의 연결
Part 1에서 5-tuple이 연결을 식별한다고 했습니다.
실제로 어떻게 동작하는지 살펴봅시다.
웹 서버가 80번 포트에서 LISTEN하고 있습니다.
1
서버: 192.168.1.100:80
클라이언트 A(10.0.0.5)가 연결합니다.
OS가 임시 포트 52001을 할당합니다.
1
연결 1: (TCP, 192.168.1.100, 80, 10.0.0.5, 52001)
같은 클라이언트 A가 또 연결합니다.
새 임시 포트 52002가 할당됩니다.
1
연결 2: (TCP, 192.168.1.100, 80, 10.0.0.5, 52002)
클라이언트 B(203.0.113.10)가 연결합니다.
1
연결 3: (TCP, 192.168.1.100, 80, 203.0.113.10, 49500)
세 연결 모두 서버의 같은 포트(80)를 사용하지만, 5-tuple이 모두 다르므로 별개의 연결입니다.
최대 동시 연결 수
이론적으로 하나의 서버 포트에서 가능한 최대 연결 수는 얼마일까요?
1
2
3
4
5
6
7
로컬 IP × 로컬 포트 × 원격 IP × 원격 포트
(고정) (고정) (변수) (변수)
원격 IP: 2^32 (IPv4)
원격 포트: 2^16
최대 = 2^32 × 2^16 = 2^48 ≈ 281조 개
물론 실제로는 메모리, 파일 디스크립터 한도, 운영체제 설정 등으로 제한됩니다.
리눅스의 기본 net.core.somaxconn은 4096입니다.
대규모 서버는 이 값을 조정합니다.
패킷의 생성: 캡슐화
애플리케이션 데이터가 네트워크로 나가기까지의 과정을 살펴봅시다.
1
2
3
4
5
6
7
8
9
애플리케이션: "GET / HTTP/1.1\r\n..."
↓
TCP: 세그먼트 생성
↓
IP: 패킷 생성
↓
이더넷: 프레임 생성
↓
물리 계층: 비트 → 전기 신호
TCP 세그먼트
TCP는 애플리케이션 데이터에 TCP 헤더를 붙여 세그먼트(Segment)를 만듭니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌────────────────────────────────────────────────────────────┐
│ TCP 헤더 (20~60 바이트) │
├────────────────────────────────────────────────────────────┤
│ 출발지 포트 (16비트) │ 목적지 포트 (16비트) │
├────────────────────────────────────────────────────────────┤
│ 시퀀스 번호 (32비트) │
├────────────────────────────────────────────────────────────┤
│ 확인 번호 (32비트) │
├────────────────────────────────────────────────────────────┤
│ 헤더길이│예약│플래그│ 윈도우 크기 │
│ (4) │(4) │(8) │ (16비트) │
├────────────────────────────────────────────────────────────┤
│ 체크섬 (16비트) │ 긴급 포인터 (16비트) │
├────────────────────────────────────────────────────────────┤
│ 옵션 (가변) │
├────────────────────────────────────────────────────────────┤
│ 데이터 (페이로드) │
└────────────────────────────────────────────────────────────┘
IP 패킷
IP 계층은 TCP 세그먼트를 페이로드로 받아 IP 헤더를 붙입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌────────────────────────────────────────────────────────────┐
│ IP 헤더 (20~60 바이트) │
├────────────────────────────────────────────────────────────┤
│버전│헤더길이│ 서비스 타입 │ 전체 길이 │
│(4) │ (4) │ (8) │ (16비트) │
├────────────────────────────────────────────────────────────┤
│ 식별자 (16비트) │플래그│ 단편 오프셋 │
│ │ (3) │ (13비트) │
├────────────────────────────────────────────────────────────┤
│ TTL (8) │ 프로토콜 (8) │ 헤더 체크섬 (16비트) │
├────────────────────────────────────────────────────────────┤
│ 출발지 IP 주소 (32비트) │
├────────────────────────────────────────────────────────────┤
│ 목적지 IP 주소 (32비트) │
├────────────────────────────────────────────────────────────┤
│ 옵션 (가변) │
├────────────────────────────────────────────────────────────┤
│ 페이로드 (TCP 세그먼트) │
└────────────────────────────────────────────────────────────┘
MTU와 세그먼테이션
MTU(Maximum Transmission Unit)는 네트워크 인터페이스가 한 번에 전송할 수 있는 최대 데이터 크기입니다.
이더넷의 표준 MTU는 1500바이트입니다.
이 값은 1980년대에 정해졌습니다.
당시 메모리 가격과 처리 능력을 고려한 절충안이었습니다.
1
2
3
4
이더넷 프레임 최대 크기: 1518바이트
- 이더넷 헤더: 14바이트
- 페이로드 (MTU): 1500바이트
- FCS: 4바이트
MSS(Maximum Segment Size)
TCP가 한 세그먼트에 담을 수 있는 최대 데이터 크기입니다.
1
2
3
MSS = MTU - IP 헤더 - TCP 헤더
= 1500 - 20 - 20
= 1460바이트
TCP 연결 시 양측은 MSS를 협상합니다.
3-Way Handshake의 SYN 패킷에 MSS 옵션을 포함합니다.
세그먼테이션
애플리케이션이 10KB를 write()하면 어떻게 될까요?
1
2
3
4
5
6
7
8
9
10
애플리케이션: write(10KB 데이터)
↓
TCP: MSS(1460바이트)로 분할
↓
세그먼트 1: 1460바이트 (seq=0)
세그먼트 2: 1460바이트 (seq=1460)
세그먼트 3: 1460바이트 (seq=2920)
...
세그먼트 7: 1460바이트 (seq=8760)
세그먼트 8: 480바이트 (seq=10220)
TCP는 데이터를 MSS 크기의 세그먼트로 나눕니다.
각 세그먼트에 시퀀스 번호가 부여됩니다.
수신측은 시퀀스 번호로 순서대로 재조립합니다.
IP 단편화
IP 패킷이 MTU가 더 작은 네트워크를 지나야 할 때 단편화(Fragmentation)가 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
출발 네트워크 중간 네트워크 도착 네트워크
MTU: 1500 MTU: 576 MTU: 1500
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│패킷 1500바이트│ ───────→ │ │ ───────→ │재조립 │
└─────────────┘ │ 단편화 필요 │ └─────────────┘
│ ↓ │
│┌───────────┐│
││단편 1(556)││ ───────→
│├───────────┤│
││단편 2(556)││ ───────→
│├───────────┤│
││단편 3(388)││ ───────→
│└───────────┘│
└─────────────┘
단편화의 문제점:
- 성능 저하: 라우터가 단편화/재조립 처리에 자원 소모
- 손실 취약: 단편 하나만 손실되어도 전체 패킷 폐기
- 보안 문제: 일부 방화벽은 단편화된 패킷을 제대로 검사하지 못함
Path MTU Discovery
단편화를 피하기 위해 경로 상의 최소 MTU를 찾는 방법입니다.
IP 헤더의 DF(Don’t Fragment) 플래그를 설정하고 패킷을 보냅니다.
경로 상의 라우터가 MTU보다 큰 패킷을 만나면, 단편화하지 않고 ICMP “Fragmentation Needed” 메시지를 반환합니다.
송신측은 이 메시지에서 해당 링크의 MTU를 알아내고, 패킷 크기를 줄여 재전송합니다.
1
2
3
4
5
6
7
8
송신측 라우터 (MTU: 1280) 수신측
│ │ │
│── 패킷 1500B, DF=1 ──→ │ │
│ │ │
│←─ ICMP: Frag Needed ───│ │
│ (MTU: 1280) │ │
│ │ │
│── 패킷 1280B, DF=1 ──→ │ ────────────────────→ │
TCP에서 단편화가 드문 이유
TCP는 MSS 협상을 합니다.
MSS는 경로 MTU를 고려하여 설정됩니다.
TCP 세그먼트가 MSS를 넘지 않으면, IP 패킷도 MTU를 넘지 않습니다.
따라서 TCP 연결에서는 IP 단편화가 거의 발생하지 않습니다.
UDP는 다릅니다.
UDP에는 MSS 같은 개념이 없습니다.
애플리케이션이 큰 데이터그램을 보내면 IP 계층에서 단편화됩니다.
재조립과 순서 보장
수신측에서 데이터를 원래 순서로 복원하는 과정을 살펴봅시다.
TCP 재조립
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
수신된 순서: 세그먼트 3, 세그먼트 1, 세그먼트 4, 세그먼트 2
수신 버퍼:
┌─────┬─────┬─────┬─────┬─────┐
│ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┘
0 1460 2920 4380 5840
세그먼트 3 도착 (seq=2920):
┌─────┬─────┬─────┬─────┬─────┐
│ │ │ [3] │ │ │ ← 아직 전달 불가
└─────┴─────┴─────┴─────┴─────┘
세그먼트 1 도착 (seq=0):
┌─────┬─────┬─────┬─────┬─────┐
│ [1] │ │ [3] │ │ │ ← 아직 전달 불가
└─────┴─────┴─────┴─────┴─────┘
세그먼트 4 도착 (seq=4380):
┌─────┬─────┬─────┬─────┬─────┐
│ [1] │ │ [3] │ [4] │ │ ← 아직 전달 불가
└─────┴─────┴─────┴─────┴─────┘
세그먼트 2 도착 (seq=1460):
┌─────┬─────┬─────┬─────┬─────┐
│ [1] │ [2] │ [3] │ [4] │ │ ← [1][2][3][4] 애플리케이션에 전달
└─────┴─────┴─────┴─────┴─────┘
TCP는 순서대로 애플리케이션에 데이터를 전달합니다.
세그먼트 3, 4가 먼저 도착해도, 세그먼트 1, 2가 올 때까지 버퍼에 보관합니다.
세그먼트 1, 2가 도착하면 연속된 데이터를 애플리케이션에 전달합니다.
IP 단편 재조립
IP 단편화된 패킷도 재조립이 필요합니다.
IP 헤더의 식별자(Identification), 플래그, 단편 오프셋 필드가 사용됩니다.
1
2
3
4
5
6
7
8
9
10
11
원본 패킷:
IP 헤더 (ID=1234) + 데이터 (4500바이트)
단편화 (MTU=1500):
단편 1: IP 헤더 (ID=1234, offset=0, MF=1) + 데이터 (1480바이트)
단편 2: IP 헤더 (ID=1234, offset=185, MF=1) + 데이터 (1480바이트)
단편 3: IP 헤더 (ID=1234, offset=370, MF=1) + 데이터 (1480바이트)
단편 4: IP 헤더 (ID=1234, offset=555, MF=0) + 데이터 (60바이트)
MF = More Fragments (더 많은 단편이 있음)
offset = 8바이트 단위의 오프셋
수신측은 같은 ID를 가진 단편들을 모아 오프셋 순서대로 재조립합니다.
마무리: 계층 간 협력
패킷이 생성되고 전달되고 재조립되는 전체 과정을 살펴보았습니다.
포트 번호는 하나의 IP 주소에서 수천 개의 동시 연결을 가능하게 합니다.
5-tuple은 각 연결을 고유하게 식별합니다.
운영체제의 소켓 테이블이 패킷을 올바른 소켓으로 라우팅합니다.
TCP는 애플리케이션 데이터를 MSS 크기로 세그먼트화합니다.
IP는 MTU에 맞춰 패킷을 단편화할 수 있지만, TCP의 MSS 협상으로 보통 단편화가 발생하지 않습니다.
수신측은 시퀀스 번호와 오프셋을 사용하여 데이터를 원래 순서로 재조립합니다.
이 모든 과정이 커널에서 자동으로 처리됩니다.
애플리케이션은 write()로 데이터를 보내고 read()로 받기만 하면 됩니다.
Part 1의 Berkeley Sockets 추상화가 이 복잡성을 모두 숨깁니다.
관련 글
시리즈
- 소켓과 전송 계층 (1) - 소켓의 탄생과 추상화
- 소켓과 전송 계층 (2) - TCP 연결의 상태 기계
- 소켓과 전송 계층 (3) - 멀티플렉싱과 패킷 흐름 (현재 글)