수신자가 감당할 수 있는 속도로 송신자의 전송량을 조절하는 것(flow control).
무엇을 막는가
빠른 송신자 ──────► 느린 수신자
수신 버퍼가 가득 참
→ 도착한 데이터를 버릴 수밖에 없다
→ 재전송 → 또 넘침 → 악순환
수신 버퍼 오버플로를 막는 것이 목적이다.
방법 — 슬라이딩 윈도우
수신자가 "내가 앞으로 받을 수 있는 바이트 수" 를 매 ACK에 실어 알린다.
TCP 헤더의 Window Size 필드 (16비트)
수신자: "ACK 1000, Window 5000"
- 1000번까지 받았고, 앞으로 5000바이트까지 더 받을 수 있다
송신자: 확인 안 된 데이터가 5000바이트를 넘지 않게 조절한다
송신 윈도우
├───────────┬────────────┬────────────┬───────────┤
│ 전송·확인 │ 전송·미확인│ 전송 가능 │ 전송 불가 │
└───────────┴────────────┴────────────┴───────────┘
└─────── 윈도우 ──────┘
ACK가 오면 윈도우가 오른쪽으로 미끄러진다 → "슬라이딩"
정지-대기(하나 보내고 ACK 기다리기) 방식보다 훨씬 빠르다 — 여러 개를 한꺼번에 보내 놓고 확인을 받기 때문이다.
혼잡 제어와의 결정적 차이 — 면접 단골
| 흐름 제어 | 혼잡 제어 | |
|---|---|---|
| 보호 대상 | 수신자 | 네트워크 전체 |
| 신호 | 수신자가 알려 주는 Window Size | 패킷 손실·지연 (추측해야 함) |
| 정보 | 명시적 | 암묵적 |
실제 전송량 = min(수신 윈도우, 혼잡 윈도우)
↑ 흐름 제어 ↑ 혼잡 제어
둘 중 작은 쪽에 맞춘다. 수신자가 여유로워도 네트워크가 막혔으면 못 보내고, 네트워크가 한가해도 수신자가 벅차면 못 보낸다.
혼잡 제어가 어려운 이유는 네트워크가 막혔다고 알려 주는 사람이 없어서 패킷 손실로 추측해야 하기 때문이다.
윈도우가 0이 되면 — 교착 위험
수신자: "Window 0" (버퍼가 꽉 찼다) 송신자: 전송 중단
수신자가 버퍼를 비우고 "Window 5000"을 보냈는데 그 패킷이 유실되면?
- 송신자는 영원히 기다린다 ← 교착
해결 — Zero Window Probe 송신자가 주기적으로 1바이트짜리 탐색 패킷을 보내 윈도우 상태를 물어본다.
Silly Window Syndrome
수신 애플리케이션이 1바이트씩 천천히 읽는다
→ 수신자: "Window 1" 광고
→ 송신자: 헤더 40바이트 + 데이터 1바이트 전송 ← 효율 2.4%
→ 반복
대응
- 수신 측(Clark) — 버퍼의 절반 또는 MSS만큼 비워지기 전에는 0으로 광고
- 송신 측(Nagle) — 작은 데이터를 모아서 한 번에 보낸다
Nagle 알고리즘과 실무 함정
"확인 안 된 데이터가 있으면, 작은 데이터는 모았다가 보낸다"
대역폭 효율은 좋지만 지연이 생긴다. 게다가 Delayed ACK(수신자가 ACK를 조금 모았다 보내는 최적화)와 겹치면
송신자: Nagle 때문에 ACK를 기다림 수신자: Delayed ACK 때문에 ACK를 미룸
- 최대 40~200ms 멈춤 ← 실시간 통신에서 치명적
socket.setTcpNoDelay(true); // Nagle 끄기
지연이 중요한 서비스(게임, 실시간 API)에서는 끄는 것이 일반적이다.
윈도우 스케일링
Window Size 필드는 16비트라 최대 65,535바이트다.
지연 100ms, 대역폭 1Gbps 인 회선에서
- 65KB / 0.1s = 5.2Mbps 밖에 못 쓴다 — ← 대역폭의 0.5%
그래서 Window Scale 옵션으로 최대 1GB까지 확장한다.
net.ipv4.tcp_window_scaling=1(기본 활성).
대역폭×지연 곱(BDP) 만큼의 윈도우가 있어야 회선을 다 쓸 수 있다는 것이 장거리 고속 전송 튜닝의 핵심 개념이다.