이전 글에서는 TCP 혼잡제어에 대해 정리했습니다.
TCP 혼잡제어는 네트워크가 너무 혼잡해지지 않도록 송신자가 전송량을 조절하는 기능입니다.
이번 글에서는 TCP 흐름제어에 대해 정리해 보겠습니다.
TCP 흐름제어는 수신자가 받을 수 있는 만큼만 데이터를 보내도록 송신자가 전송량을 조절하는 기능입니다.
TCP는 데이터를 안정적으로 보내기 위해 사용되는 전송 계층 프로토콜입니다.
TCP는 데이터가 중간에 사라지면
다시 보내고, 순서가 바뀌면 다시 정리하며, 상대방이 받을 수 있는 속도에 맞춰 전송하는 프로토콜입니다.
여기서 상대방이 받을 수 있는 속도에 맞춰 전송하는 기능이 바로 흐름제어입니다.
송신자가 아무리 빠르게 데이터를 보낼 수 있어도 수신자가 데이터를 처리하지 못하면 문제가 발생할 수 있습니다.
수신자가 처리하지 못한 데이터는 수신 버퍼에 쌓입니다.
수신 버퍼가 가득 찬 상태에서 데이터가 계속 도착하면 수신자는 더 이상 데이터를 받기 어렵습니다.
그래서 TCP는 수신자의 버퍼 상태를 확인하면서 데이터를 전송합니다.
이번 글에서는
TCP 흐름제어가 무엇인지, 왜 필요한지, rwnd가 어떤 역할을 하는지,
그리고 정상 수신 상태와 흐름제어가 발생한 상태가 어떻게 다른지 확인해 보겠습니다.
TCP 흐름제어란
TCP 흐름제어는 송신자가 수신자의 처리 가능 상태에 맞춰 데이터를 보내도록 조절하는 기능입니다.
여기서 중요한 기준은 중간 네트워크의 상태가 아니라 수신자의 상태입니다.
수신자는 데이터를 받으면 먼저 커널의 수신 버퍼에 저장합니다.
이후 애플리케이션이 수신 버퍼에 있는 데이터를 읽어 갑니다.
애플리케이션이 데이터를 빠르게 읽으면 수신 버퍼에는 여유 공간이 생깁니다.
반대로 애플리케이션이 데이터를 천천히 읽으면 수신 버퍼가 점점 차게 됩니다.
수신 버퍼가 부족해지면 수신자는 TCP Window Size 값을 줄여서 송신자에게 알려 줍니다.
송신자는 이 값을 보고 한 번에 보낼 수 있는 데이터 양을 조절합니다.
이 과정이 TCP 흐름제어입니다.
TCP 흐름제어는 수신자의 버퍼가 넘치지 않도록 송신 속도를 조절하는 기능입니다.
흐름제어가 필요한 이유
송신자와 수신자의 처리 속도는 항상 같지 않습니다.
송신자는 빠르게 데이터를 보낼 수 있지만 수신자는 데이터를 천천히 처리할 수 있습니다.
예를 들어 수신자의 CPU가 바쁘거나 수신 애플리케이션이 데이터를 늦게 읽는 상황이 있을 수 있습니다.
이때 송신자가 계속 많은 데이터를 보내면 수신 버퍼가 가득 찰 수 있습니다.
수신 버퍼가 가득 차면 새로 도착한 데이터를 정상적으로 처리하기 어려워집니다.
이런 상황이 반복되면 송신자는 더 이상 데이터를 마음대로 보낼 수 없게 됩니다.
TCP 흐름제어는 이런 문제를 막기 위해 필요합니다.
송신자가 빠르다고 해서 무조건 많이 보내는 것이 아니라 수신자가 받을 수 있는 만큼만 보내도록 제한하는 기능입니다.
결국 TCP 흐름제어는 수신자를 보호하기 위한 전송량 조절 기능입니다.
흐름제어와 혼잡제어의 차이
TCP를 공부할 때 흐름제어와 혼잡제어를 헷갈리는 경우가 많습니다.
두 기능 모두 송신자의 전송량을 조절한다는 점에서는 비슷합니다.
하지만 기준이 다릅니다.
흐름제어는 수신자가 받을 수 있는 만큼만 보내는 기능입니다.
혼잡제어는 네트워크가 감당할 수 있는 만큼만 보내는 기능입니다.
흐름제어의 기준은 수신자의 버퍼 상태입니다.
혼잡제어의 기준은 중간 네트워크의 혼잡 상태입니다.
흐름제어에서는 receiver window인 rwnd가 중요합니다.
혼잡제어에서는 congestion window인 cwnd가 중요합니다.
rwnd는 수신자가 송신자에게 알려 주는 수신 가능 크기입니다.
cwnd는 송신자가 네트워크 혼잡 상태를 보고 스스로 조절하는 전송 가능 크기입니다.
실제로 TCP가 한 번에 보낼 수 있는 데이터 양은 rwnd와 cwnd 중 더 작은 값의 영향을 받습니다.
수신자는 충분히 받을 수 있어도 네트워크가 혼잡하면 많이 보내지 못합니다.
반대로 네트워크가 여유로워도 수신자의 버퍼가 부족하면 많이 보내지 못합니다.
따라서 흐름제어는 수신자를 보호하는 기능이고, 혼잡제어는 네트워크를 보호하는 기능입니다.
rwnd란
TCP 흐름제어에서 가장 중요한 값은 rwnd입니다.
rwnd는 Receiver Window의 줄임말입니다.
우리말로는 수신 윈도우입니다.
rwnd는 수신자가 현재 추가로 받을 수 있는 데이터 양을 의미합니다.
수신자는 ACK를 보낼 때 TCP 헤더의 Window Size 필드에 이 값을 담아 보냅니다.
송신자는 이 값을 보고 수신자가 어느 정도까지 데이터를 받을 수 있는지 판단합니다.
rwnd가 크면 수신자의 버퍼에 여유가 많다는 의미입니다.
rwnd가 작으면 수신자의 버퍼에 여유가 적다는 의미입니다.
rwnd가 0이면 수신자가 현재 더 이상 데이터를 받을 공간이 없다는 의미입니다.
결국 TCP 흐름제어는 rwnd를 기준으로 송신량을 조절하는 과정입니다.
TCP Window Size
TCP 헤더에는 Window Size 필드가 있습니다.
Window Size 필드는 수신자가 현재 받을 수 있는 데이터 양을 송신자에게 알려 주기 위해 사용됩니다.
TCP는 데이터를 바이트 단위의 연속된 흐름으로 처리합니다.
수신자는 ACK 번호를 통해 어디까지 데이터를 받았는지 알려 줍니다.
그리고 Window Size를 통해 ACK 번호 이후로 얼마나 더 받을 수 있는지 알려 줍니다.
예를 들어 ACK 번호가 1001이고 Window Size가 4000이면
수신자는 1001번 바이트부터 5000번 바이트까지 받을 수 있다는 의미입니다.
송신자는 이 범위 안에서 데이터를 보낼 수 있습니다.
이 범위를 넘어서 너무 많은 데이터를 보내면 수신자의 버퍼가 감당하기 어려워질 수 있습니다.
따라서 Window Size는 TCP 흐름제어에서 송신자가 지켜야 하는 수신자의 허용 범위입니다.
Sliding Window
TCP 흐름제어는 Sliding Window 방식으로 동작합니다.
Sliding Window는 수신자가 받을 수 있는 데이터 범위가 통신 상황에 따라 계속 이동하는 구조입니다.
수신자가 데이터를 받고 애플리케이션이 그 데이터를 읽어 가면 수신 버퍼에 여유 공간이 생깁니다.
그러면 수신자는 더 많은 데이터를 받을 수 있게 됩니다.
이때 수신 가능한 범위가 앞으로 이동합니다.
송신자는 현재 허용된 윈도우 범위 안에서 데이터를 보냅니다.
ACK가 돌아오고 수신 윈도우가 갱신되면 송신자는 다음 데이터를 이어서 보냅니다.
이처럼 TCP는 전체 데이터를 한 번에 모두 보내는 방식이 아닙니다.
현재 수신자가 허용한 범위 안에서 데이터를 보내고,
ACK와 Window Size를 기준으로 전송 범위를 계속 이동시키는 방식입니다.
이 구조가 TCP의 Sliding Window 방식입니다.
Zero Window와 Window Update
Zero Window는 수신자가 더 이상 받을 공간이 없다고 알리는 상태입니다.
수신자의 버퍼가 가득 차면 수신자는 Window Size를 0으로 광고할 수 있습니다.
이때 송신자는 새로운 데이터를 계속 보내면 안 됩니다.
송신자는 수신자의 버퍼에 여유 공간이 생길 때까지 기다려야 합니다.
Zero Window는 TCP 연결이 끊어진 상태가 아닙니다.
Zero Window는 수신자가 잠시 데이터를 받을 수 없는 상태입니다.
수신 애플리케이션이 다시 데이터를 읽어 가면 수신 버퍼에 공간이 생깁니다.
그러면 수신자는 Window Size를 다시 늘려서 송신자에게 알려 줍니다.
이것을 Window Update라고 합니다.
Zero Window가 수신자가 받을 수 없다는 알림이라면 Window Update는 수신자가 다시 받을 수 있다는 알림입니다.
Zero Window Probe
Zero Window 상태에서 송신자가 계속 기다리기만 하면 문제가 생길 수 있습니다.
수신자의 윈도우가 다시 열렸다는 Window Update 패킷이 중간에 사라질 수 있기 때문입니다.
그래서 송신자는 일정 시간이 지나면 아주 작은 데이터를 보내 수신자의 상태를 확인합니다.
이 동작을 Zero Window Probe라고 합니다.
Probe는 확인하거나 탐지한다는 의미입니다.
Zero Window Probe는 송신자가 수신자에게 다시 받을 수 있는 상태인지 확인하는 패킷입니다.
수신자가 아직 받을 수 없다면 다시 Window Size 0을 알려 줍니다.
수신자가 받을 수 있는 상태가 되었다면 Window Update를 통해 새로운 Window Size를 알려 줍니다.
따라서 Zero Window Probe는 Zero Window 상태가 계속 유지되는지 확인하기 위한 TCP 동작입니다.
Window Scaling
TCP 헤더의 Window Size 필드는 기본적으로 16비트입니다.
16비트로 표현할 수 있는 최대값은 65,535입니다.
기본 Window Size 필드만 사용하면 약 64KB 정도의 수신 윈도우까지만 표현할 수 있습니다.
과거의 느린 네트워크에서는 이 크기로도 충분한 경우가 많았습니다.
하지만 현대 네트워크에서는 64KB가 너무 작을 수 있습니다.
특히 대역폭이 크고 RTT가 긴 환경에서는 더 큰 윈도우가 필요합니다.
이 문제를 해결하기 위해 TCP Window Scaling 옵션이 사용됩니다.
Window Scaling은 TCP Window Size 값을 일정 배수로 확장해 더 큰 수신 윈도우를 표현하는 기능입니다.
Window Scaling 옵션은 TCP 3-way handshake 과정에서 협상됩니다.
SYN과 SYN/ACK 단계에서 서로 Window Scaling 사용 여부와 배율을 주고받습니다.
따라서 패킷을 분석할 때는
TCP 헤더에 적힌 원래 Window Size 값과 실제 계산된 Window Size 값을 구분해야 합니다.
tshark에서는 tcp.window_size_value와 tcp.window_size를 구분해서 볼 수 있습니다.
이번 실험에서는
흐름제어 상태를 직관적으로 확인하기 위해 계산된 윈도우 크기인 tcp.window_size를 중심으로 확인하겠습니다.
이번 실험의 목표
이번 실험의 목표는 정상 수신 상태와 흐름제어가 발생한 상태의 차이를 확인하는 것입니다.
정상 수신 상태에서는 Server가 데이터를 빠르게 읽습니다.
Server가 데이터를 빠르게 읽으면 수신 버퍼에 여유 공간이 유지됩니다.
따라서 Window Full이나 Zero Window 같은 흐름제어 이벤트가 거의 발생하지 않습니다.
반대로 흐름제어 발생 상태에서는 Server가 데이터를 일부러 천천히 읽습니다.
Server가 데이터를 천천히 읽으면 수신 버퍼가 점점 찹니다.
수신 버퍼가 부족해지면 Server는 Client에게 더 작은 Window Size를 광고합니다.
최종적으로 받을 공간이 없어지면 Server는 Window Size를 0으로 광고합니다.
이 상태가 Zero Window입니다.
이번 실험에서는 먼저 정상 수신 상태를 확인하고,
이후 느린 수신 상태에서 Window Full과 Zero Window가 발생하는지 확인하겠습니다.
실험 환경
이번 실험은 Linux VM 2대를 사용했습니다.
하나는 Client 역할입니다.
다른 하나는 Server 역할입니다.
Client IP는 192.168.20.9입니다.
Server IP는 192.168.20.10입니다.
TCP 포트는 5001번을 사용했습니다.
Client는 Server로 데이터를 빠르게 보냅니다.
Server는 첫 번째 실험에서는 데이터를 빠르게 읽습니다.
Server는 두 번째 실험에서는 데이터를 일부러 천천히 읽습니다.
두 실험 모두 tcpdump로 패킷을 캡처하고, tshark로 흐름제어 이벤트를 확인합니다.
실험 준비
Client와 Server에 필요한 패키지를 설치합니다.
sudo apt update
sudo apt install -y tcpdump tshark python3
apt update는 설치 가능한 패키지 목록을 최신 상태로 갱신하는 명령어입니다.
-y 옵션은 설치 여부를 물어볼 때 자동으로 yes를 입력한다는 의미입니다.
tcpdump는 패킷을 캡처해서 pcap 파일로 저장하는 도구입니다.
tshark는 pcap 파일을 터미널에서 분석하는 도구입니다.
python3는 실험용 TCP 서버와 클라이언트를 실행하기 위한 도구입니다.
먼저 네트워크 인터페이스 이름을 확인합니다.
ip -br addr
-br 옵션은 brief의 줄임말입니다.
결과를 간단하게 보여주는 옵션입니다.
출력 결과에서 실제 통신에 사용하는 인터페이스 이름을 확인합니다.
제 환경에서는 enX0 인터페이스를 사용했습니다.
정상 수신 상태 만들기
먼저 정상 수신 상태를 만듭니다.
정상 수신 상태에서는 Server가 Client로부터 받은 데이터를 빠르게 읽습니다.
Server가 데이터를 빠르게 읽으면 수신 버퍼에 여유 공간이 계속 생깁니다.
따라서 흐름제어 이벤트가 발생하지 않는 것이 정상입니다.
Server에서 normal_server.py 파일을 생성합니다.
vi normal_server.py
아래 코드를 입력합니다.
import socket
HOST = "0.0.0.0"
PORT = 5001
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(1)
print("normal server listening")
conn, addr = server_socket.accept()
print("connected by", addr)
total = 0
while True:
data = conn.recv(65536)
if not data:
break
total += len(data)
print("total received bytes:", total)
conn.close()
server_socket.close()
socket.socket은 TCP 통신에 사용할 소켓을 만드는 부분입니다.
AF_INET은 IPv4를 사용한다는 의미입니다.
SOCK_STREAM은 TCP를 사용한다는 의미입니다.
SO_REUSEADDR은 같은 포트를 다시 사용할 수 있게 하는 옵션입니다.
bind는 서버가 사용할 IP 주소와 포트 번호를 지정하는 부분입니다.
0.0.0.0은 모든 네트워크 인터페이스에서 접속을 받겠다는 의미입니다.
5001은 TCP 서버가 대기할 포트 번호입니다.
listen은 클라이언트 접속을 기다리는 상태로 만드는 부분입니다.
accept는 실제 클라이언트 연결을 받는 부분입니다.
recv(65536)은 데이터를 최대 65536바이트씩 읽겠다는 의미입니다.
빠른 송신자 만들기
Client에서는 Server로 데이터를 빠르게 보내는 프로그램을 사용합니다.
Client에서 fast_client.py 파일을 생성합니다.
vi fast_client.py
아래 코드를 입력합니다.
import socket
SERVER_IP = "192.168.20.10"
PORT = 5001
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((SERVER_IP, PORT))
data = b"A" * (50 * 1024 * 1024)
sent = 0
while sent < len(data):
n = client_socket.send(data[sent:sent + 65536])
sent += n
print("sent bytes:", sent)
client_socket.close()
SERVER_IP에는 Server의 IP 주소를 입력합니다.
PORT는 Server에서 대기 중인 TCP 포트 번호입니다.
connect는 Server와 TCP 연결을 맺는 부분입니다.
data는 50MB 크기의 데이터를 만드는 부분입니다.
b"A"는 바이트 데이터를 의미합니다.
50 * 1024 * 1024는 50MB를 의미합니다.
send는 TCP 소켓으로 데이터를 보내는 함수입니다.
65536은 한 번에 최대 64KB 정도씩 보내도록 자르는 값입니다.
정상 상태 패킷 캡처
정상 수신 상태의 패킷을 캡처합니다.
Server 터미널 1에서 normal_server.py를 실행합니다.
python3 normal_server.py
Server 터미널 2에서 tcpdump를 실행합니다.
sudo tcpdump -i enX0 -nn -s 0 -w normal_tcp.pcap "host 192.168.20.9 and tcp port 5001"
-i 옵션은 interface의 줄임말입니다.
-nn 옵션은 IP 주소와 포트 번호를 이름으로 변환하지 않고 숫자 그대로 보여 주는 옵션입니다.
-s 0 옵션은 패킷을 잘라서 저장하지 않고 전체 패킷을 저장한다는 의미입니다.
-w 옵션은 캡처 결과를 파일로 저장한다는 의미입니다.
normal_tcp.pcap은 정상 수신 상태의 패킷을 저장할 파일 이름입니다.
host 192.168.20.9는 Client와 오가는 패킷만 캡처한다는 의미입니다.
tcp port 5001은 TCP 5001번 포트와 관련된 패킷만 캡처한다는 의미입니다.
tcpdump를 실행한 뒤 Client에서 fast_client.py를 실행합니다.
python3 fast_client.py
Client의 전송이 끝나면 Server의 tcpdump 화면에서 Ctrl + C를 눌러 캡처를 종료합니다.
정상 상태 tshark 확인
정상 상태에서 흐름제어 이벤트가 발생했는지 확인합니다.
tshark -r normal_tcp.pcap \
-Y "tcp.analysis.window_full || tcp.analysis.zero_window || tcp.analysis.zero_window_probe || tcp.analysis.window_update" \
-T fields \
-e frame.number \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e tcp.window_size \
-e _ws.col.Info \
-E header=y \
-E separator=/t | column -t -s $'\t'
-r 옵션은 read의 의미입니다.
-Y 옵션은 display filter를 적용하는 옵션입니다.
tcp.analysis.window_full은 수신자가 광고한 윈도우가 거의 다 찬 상태를 찾는 조건입니다.
tcp.analysis.zero_window는 수신자가 더 이상 받을 공간이 없다고 알린 패킷을 찾는 조건입니다.
tcp.analysis.zero_window_probe는
송신자가 Zero Window 상태에서 수신자의 상태를 확인하는 패킷을 찾는 조건입니다.
tcp.analysis.window_update는 수신자가 다시 받을 수 있는 공간이 생겼다고 알린 패킷을 찾는 조건입니다.
-T fields는 원하는 필드만 출력하겠다는 의미입니다.
-e 옵션은 출력할 필드를 지정하는 옵션입니다.
frame.number는 패킷 번호입니다.
frame.time_relative는 캡처 시작 시점부터의 상대 시간입니다.
ip.src는 출발지 IP 주소입니다.
ip.dst는 목적지 IP 주소입니다.
tcp.window_size는 계산된 TCP Window Size 값입니다.
_ws.col.Info는 tshark에서 보여 주는 Info 컬럼입니다.
-E header=y는 첫 줄에 필드 이름을 출력한다는 의미입니다.
-E separator=/t는 필드 구분자를 탭으로 지정한다는 의미입니다.
column -t는 출력 결과를 보기 좋게 정렬하는 명령어입니다.
-s $'\t'는 탭을 기준으로 열을 나누겠다는 의미입니다.
정상 상태 tshark 결과 해석
정상 상태의 pcap 파일을 tshark로 확인했습니다.
이번 명령어는 전체 TCP 패킷을 출력하는 것이 아니라 흐름제어와 관련된 패킷만 필터링하는 명령어입니다.
실행 결과 필드 이름만 출력되고 실제 패킷은 출력되지 않았습니다.
이는 정상 상태에서
Window Full, Zero Window, Zero Window Probe, Window Update가 발생하지 않았다는 의미입니다.
정상 수신 상태에서는 Server가 데이터를 빠르게 읽기 때문에 수신 버퍼에 여유 공간이 유지됩니다.
수신 버퍼에 여유 공간이 유지되면 Server는 Window Size를 0으로 광고할 필요가 없습니다.
따라서 정상 상태에서는 Zero Window가 나타나지 않는 것이 정상입니다.
느린 수신 상태 만들기
이번에는 Server가 데이터를 일부러 천천히 읽도록 만들어 흐름제어 상황을 발생시킵니다.
정상 상태에서는 Server가 데이터를 빠르게 읽었기 때문에 수신 버퍼에 여유 공간이 유지되었습니다.
그래서 tshark 결과에서도 Window Full이나 Zero Window가 출력되지 않았습니다.
이번에는 반대로 Server가 데이터를 매우 천천히 읽게 만들어 수신 버퍼가 부족해지는 상황을 확인하겠습니다.
Server에서 다음 파일을 생성합니다.
vi slow_server.py
slow_server.py는 느린 수신자 역할을 할 Python 파일입니다.
아래 코드를 입력합니다.
import socket
import time
HOST = "0.0.0.0"
PORT = 5001
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
server_socket.bind((HOST, PORT))
server_socket.listen(1)
print("slow server listening")
conn, addr = server_socket.accept()
print("connected by", addr)
total = 0
while True:
data = conn.recv(1)
if not data:
break
total += len(data)
if total % 100 == 0:
print("received bytes:", total)
time.sleep(0.05)
conn.close()
server_socket.close()
이 코드는 TCP 서버를 만들고 Client의 접속을 기다리는 코드입니다.
socket.socket은 TCP 통신에 사용할 소켓을 만드는 부분입니다.
AF_INET은 IPv4를 사용한다는 의미입니다.
SOCK_STREAM은 TCP를 사용한다는 의미입니다.
SO_REUSEADDR은 같은 포트를 다시 사용할 수 있게 하는 옵션입니다.
SO_RCVBUF는 수신 버퍼 크기와 관련된 옵션입니다.
4096은 수신 버퍼를 작게 설정해 흐름제어 상황을 더 쉽게 만들기 위한 값입니다.
bind는 서버가 사용할 IP 주소와 포트 번호를 지정하는 부분입니다.
0.0.0.0은 모든 네트워크 인터페이스에서 접속을 받겠다는 의미입니다.
5001은 TCP 서버가 대기할 포트 번호입니다.
listen은 클라이언트 접속을 기다리는 상태로 만드는 부분입니다.
accept는 실제 클라이언트 연결을 받는 부분입니다.
이번 코드에서 가장 중요한 부분은 recv(1)과 time.sleep(0.05)입니다.
recv(1)은 데이터를 1바이트씩만 읽겠다는 의미입니다.
time.sleep(0.05)는 데이터를 읽은 뒤 0.05초 동안 쉬겠다는 의미입니다.
이 서버는 데이터를 매우 천천히 읽는 수신자입니다.
Server가 데이터를 천천히 읽으면 수신 버퍼가 빠르게 비워지지 않습니다.
수신 버퍼가 부족해지면 Server는 Client에게 더 작은 Window Size를 광고하게 됩니다.
최종적으로 받을 공간이 없어지면 Window Size를 0으로 광고할 수 있습니다.
이 상태가 Zero Window입니다.
정상 상태에서는 normal_server.py가 데이터를 빠르게 읽는 서버 역할을 했습니다.
이번에는 slow_server.py가 데이터를 천천히 읽는 서버 역할을 합니다.
흐름제어 상태 패킷 캡처
이제 흐름제어가 발생하는 상태의 패킷을 캡처합니다.
Server 터미널 1에서는 slow_server.py를 실행해 둡니다.
python3 slow_server.py
Server 터미널 2에서는 tcpdump를 실행합니다.
sudo tcpdump -i enX0 -nn -s 0 -w tcp_flow_control.pcap "host 192.168.20.9 and tcp port 5001"
-i 옵션은 interface의 줄임말입니다.
enX0는 패킷을 캡처할 네트워크 인터페이스 이름입니다.
-nn 옵션은 IP 주소와 포트 번호를 이름으로 변환하지 않고 숫자 그대로 보여 주는 옵션입니다.
-s 0 옵션은 패킷을 잘라서 저장하지 않고 전체 패킷을 저장한다는 의미입니다.
-w 옵션은 캡처 결과를 파일로 저장한다는 의미입니다.
tcp_flow_control.pcap은 흐름제어 상태의 패킷을 저장할 파일 이름입니다.
host 192.168.20.9는 Client와 오가는 패킷만 캡처한다는 의미입니다.
tcp port 5001은 TCP 5001번 포트와 관련된 패킷만 캡처한다는 의미입니다.
tcpdump를 실행한 뒤 Client에서 fast_client.py를 실행합니다.
python3 fast_client.py
이때 Client의 출력이 처음에는 빠르게 올라가다가 중간에 느려지거나 멈춘 것처럼 보일 수 있습니다.
이것은 Client 프로그램이 잘못된 것이 아닙니다.
Server가 데이터를 너무 천천히 읽기 때문에 수신 버퍼에 여유 공간이 부족해진 상태일 수 있습니다.
Client가 데이터를 더 보내고 싶어도 Server가 받을 수 있는 공간을 충분히 제공하지 못하는 상황입니다.
이 상황이 TCP 흐름제어가 발생한 상태입니다.
Window Full이나 Zero Window를 확인할 만큼 캡처가 진행되면
tcpdump 화면에서 Ctrl + C를 눌러 캡처를 종료합니다.
흐름제어 상태 tshark 확인
이제 tcp_flow_control.pcap 파일을 tshark로 읽어서 흐름제어 이벤트를 확인합니다.
정상 상태에서 사용한 것과 같은 필터를 사용합니다.
같은 필터를 사용하는 이유는 정상 상태와 흐름제어 상태의 차이를 명확하게 보기 위해서입니다.
정상 상태에서는 해당 필터 결과가 비어 있었습니다.
이번에는 Server가 데이터를 천천히 읽었기 때문에 Window Full이나 Zero Window가 나타나는지 확인합니다.
tshark -r tcp_flow_control.pcap \
-Y "tcp.analysis.window_full || tcp.analysis.zero_window || tcp.analysis.zero_window_probe || tcp.analysis.window_update" \
-T fields \
-e frame.number \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e tcp.window_size \
-e _ws.col.Info \
-E header=y \
-E separator=/t 2>/dev/null | column -t -s $'\t'
-r 옵션은 read의 의미입니다.
-Y 옵션은 display filter를 적용하는 옵션입니다.
tcp.analysis.window_full은 수신자가 광고한 윈도우가 거의 다 찬 상태를 찾는 조건입니다.
tcp.analysis.zero_window는 수신자가 더 이상 받을 공간이 없다고 알린 패킷을 찾는 조건입니다.
tcp.analysis.zero_window_probe는
송신자가 Zero Window 상태에서 수신자의 상태를 확인하는 패킷을 찾는 조건입니다.
tcp.analysis.window_update는 수신자가 다시 받을 수 있는 공간이 생겼다고 알린 패킷을 찾는 조건입니다.
-T fields는 원하는 필드만 출력하겠다는 의미입니다.
-e 옵션은 출력할 필드를 지정하는 옵션입니다.
frame.number는 패킷 번호입니다.
frame.time_relative는 캡처 시작 시점부터의 상대 시간입니다.
ip.src는 출발지 IP 주소입니다.
ip.dst는 목적지 IP 주소입니다.
tcp.window_size는 계산된 TCP Window Size 값입니다.
_ws.col.Info는 tshark에서 보여 주는 Info 컬럼입니다.
-E header=y는 첫 줄에 필드 이름을 출력한다는 의미입니다.
-E separator=/t는 필드 구분자를 탭으로 지정한다는 의미입니다.
2>/dev/null은 root 실행 경고 같은 메시지를 화면에 출력하지 않고 버린다는 의미입니다.
column -t는 출력 결과를 보기 좋게 정렬하는 명령어입니다.
-s $'\t'는 탭을 기준으로 열을 나누겠다는 의미입니다.
흐름제어 상태 tshark 결과 해석

흐름제어 상태의 tshark 결과에서는 Window Full과 Zero Window를 확인할 수 있었습니다.
먼저 frame 4를 확인합니다.
frame 4는 Client인 192.168.20.9가 Server인 192.168.20.10으로 데이터를 보낸 패킷입니다.
이 패킷의 tcp.window_size는 64256입니다.
그리고 _ws.col.Info 필드에는 TCP Window Full이 표시되었습니다.
TCP Window Full은 송신자인 Client가 수신자인 Server가 광고한 수신 윈도우를 거의 다 채운 상태를 의미합니다.
Client가 Server로 데이터를 보내는 과정에서 Server가 받을 수 있다고 알려 준 공간이 거의 다 사용된 상태입니다.
frame 4의 Info를 보면 50696번 포트에서 5001번 포트로 데이터가 전송되고 있습니다.
여기서 50696은 Client가 사용하는 임시 포트입니다.
5001은 Server가 대기 중인 TCP 포트입니다.
또한 Len=2896으로 표시되어 있습니다.
Len=2896은 이 패킷에 2896바이트의 TCP 데이터가 들어 있다는 의미입니다.
frame 4는 Client가 Server로 실제 데이터를 보내고 있는 패킷입니다.
그런데 이 패킷이 TCP Window Full로 표시되었기 때문에 Server의 수신 윈도우가 거의 다 찬 상태라고 볼 수 있습니다.
다음으로 frame 5를 확인합니다.
frame 5는 Server인 192.168.20.10이 Client인 192.168.20.9로 보낸 패킷입니다.
이 패킷의 tcp.window_size는 0입니다.
그리고 _ws.col.Info 필드에는 TCP ZeroWindow가 표시되었습니다.
TCP ZeroWindow는 수신자인 Server가 더 이상 받을 공간이 없다고 송신자인 Client에게 알리는 상태입니다.
frame 5의 Info를 보면 5001번 포트에서 50696번 포트로 ACK가 전송되고 있습니다.
이는 Server가 Client에게 응답을 보내는 방향입니다.
또한 Win=0으로 표시되어 있습니다.
Win=0은 Server가 현재 받을 수 있는 수신 윈도우 크기를 0으로 광고했다는 의미입니다.
Server가 Client에게 “지금은 더 이상 데이터를 받을 공간이 없습니다”라고 알린 것입니다.
이후 frame 7, frame 10, frame 12, frame 14, frame 16, frame 18, frame 20에서도
같은 방향의 TCP ZeroWindow 패킷이 반복해서 나타납니다.
이 패킷들은 모두 Server인 192.168.20.10에서 Client인 192.168.20.9로 전송되었습니다.
그리고 모두 tcp.window_size가 0으로 표시되었습니다.
이는 Server가 계속해서 받을 공간이 없다는 상태를 Client에게 알리고 있다는 의미입니다.
정리하면 흐름은 다음과 같습니다.
먼저 Client가 Server로 데이터를 전송했습니다.
그 과정에서 Client가 Server의 수신 윈도우를 거의 다 채우는 Window Full 상태가 발생했습니다.
그 직후 Server는 Client에게 Window Size 0을 광고했습니다.
이 상태가 Zero Window입니다.
Server가 데이터를 너무 천천히 읽어서 수신 버퍼가 부족해졌고,
그 결과 Client의 전송이 제한되는 상황이 발생했습니다.
따라서 이번 결과를 통해 TCP 흐름제어가 실제로 동작했음을 확인할 수 있습니다.
이번 결과에서는 Zero Window Probe와 Window Update는 바로 확인되지 않았습니다.
이것은 실험이 실패했다는 의미가 아닙니다.
Zero Window Probe는 Zero Window 상태가 일정 시간 지속될 때
Client가 Server의 상태를 확인하기 위해 보내는 패킷입니다.
Window Update는 Server의 수신 버퍼에 다시 충분한 여유 공간이 생겼을 때 Server가 보내는 패킷입니다.
이번 실험에서는 Server가 데이터를 1바이트씩 천천히 읽고 있었기 때문에
버퍼에 충분한 여유 공간이 바로 생기지 않았을 수 있습니다.
따라서 이번 실험에서는 Window Full과 Zero Window를 중심으로 흐름제어 발생 여부를 판단했습니다.
정상 상태와 흐름제어 상태의 차이
정상 상태와 흐름제어 상태의 가장 큰 차이는 수신 버퍼에 여유 공간이 유지되는지 여부입니다.
정상 상태에서는 Server가 데이터를 빠르게 읽었습니다.
Server가 데이터를 빠르게 읽으면 수신 버퍼에 여유 공간이 계속 생깁니다.
그래서 Server는 Client에게 Window Size 0을 광고할 필요가 없습니다.
정상 상태의 tshark 결과에서 흐름제어 관련 패킷이 출력되지 않은 이유가 바로 이것입니다.
반면 흐름제어 상태에서는 Server가 데이터를 매우 천천히 읽었습니다.
이번 실험에서는 Server가 recv(1)로 데이터를 1바이트씩만 읽고, time.sleep(0.05)로 잠시 쉬도록 만들었습니다.
이렇게 하면 Server의 수신 버퍼가 빠르게 비워지지 않습니다.
Client는 계속 데이터를 보내려고 하기 때문에 Server의 수신 버퍼는 점점 차게 됩니다.
그 결과 Client가 Server의 수신 윈도우를 거의 다 채운 Window Full 상태가 발생했습니다.
이후 Server는 더 이상 받을 공간이 없다는 의미로 Window Size 0을 광고했습니다.
이 상태가 Zero Window입니다.
정상 상태에서는 Server가 데이터를 잘 읽기 때문에 흐름제어 이벤트가 나타나지 않았습니다.
반대로 흐름제어 상태에서는 Server가 데이터를 너무 천천히 읽었기 때문에
Window Full과 Zero Window가 나타났습니다.
이번 실험에서 확인한 핵심은 송신 속도가 느려졌다고 해서 항상 네트워크 혼잡이라고 판단하면 안 된다는 점입니다.
이번 경우에는 네트워크가 혼잡해서 막힌 것이 아닙니다.
수신자인 Server가 데이터를 처리하지 못해 수신 버퍼가 부족해졌고,
그 결과 송신자인 Client가 전송을 제한받은 상황입니다.
따라서 이번 결과는 혼잡제어가 아니라 흐름제어에 의한 전송 제한으로 볼 수 있습니다.
실험할 때 주의할 점
첫 번째로 Zero Window Probe와 Window Update가 항상 함께 보이는 것은 아닙니다.
Zero Window Probe는 Zero Window 상태가 일정 시간 지속될 때 나타날 수 있습니다.
Window Update는 Server의 수신 버퍼에 다시 충분한 여유 공간이 생겼을 때 나타날 수 있습니다.
따라서 Window Full과 Zero Window만 확인되어도 흐름제어 발생 여부를 판단할 수 있습니다.
이번 실험에서도 Window Full 이후 Zero Window가 나타났기 때문에 흐름제어가 동작했다고 볼 수 있습니다.
두 번째로 Zero Window가 항상 바로 보이는 것은 아닙니다.
Linux는 TCP 수신 버퍼를 자동으로 조절할 수 있기 때문에
환경에 따라 Zero Window 대신 Window Full만 보일 수 있습니다.
세 번째로 같은 장비 안에서 127.0.0.1로 실험하면 결과가 다르게 나올 수 있습니다.
loopback 환경은 실제 네트워크보다 너무 빠르게 처리될 수 있습니다.
가능하면 Client VM과 Server VM을 분리해서 실험하는 것이 좋습니다.
네 번째로 tcp.window_size_value와 tcp.window_size를 구분해야 합니다.
tcp.window_size_value는 TCP 헤더에 들어 있는 원래 값입니다.
tcp.window_size는 Window Scaling이 반영된 계산 값입니다.
실제 수신 윈도우 크기를 볼 때는 tcp.window_size를 확인하는 것이 더 직관적입니다.
다섯 번째로 tshark의 tcp.analysis 값이나 _ws.col.Info 값은 TCP 헤더에 직접 들어 있는 값이 아닙니다.
이 값들은 tshark가 패킷 흐름을 보고 분석해서 붙여 주는 정보입니다.
따라서 분석 결과를 볼 때는 패킷 방향과 Window Size를 함께 확인해야 합니다.
여섯 번째로 송신 속도가 느려졌다고 해서 무조건 네트워크 혼잡이라고 판단하면 안 됩니다.
네트워크 혼잡이라면 패킷 손실, RTT 증가, 재전송, cwnd 변화가 중요합니다.
흐름제어 문제라면 수신 윈도우인 rwnd와 Window Size 변화가 중요합니다.
이번 실험에서는 네트워크가 혼잡해서 송신이 제한된 것이 아니라
Server가 데이터를 천천히 읽어서 수신 윈도우가 줄어든 상황을 확인한 것입니다.
정리
TCP 흐름제어는 수신자가 받을 수 있는 만큼만 데이터를 보내도록 송신자의 전송량을 조절하는 기능입니다.
흐름제어의 핵심 기준은 수신자의 버퍼 상태입니다.
수신자는 TCP Window Size 값을 통해 자신이 얼마나 더 받을 수 있는지 송신자에게 알려 줍니다.
송신자는 이 값을 보고 전송량을 조절합니다.
rwnd는 수신자가 현재 받을 수 있는 데이터 양입니다.
rwnd가 줄어들면 송신자는 한 번에 보낼 수 있는 데이터 양을 줄여야 합니다.
rwnd가 0이 되면 Zero Window 상태입니다.
Zero Window 상태에서는 송신자가 새로운 데이터를 계속 보내면 안 됩니다.
송신자는 Zero Window Probe를 통해 수신자가 다시 받을 수 있는 상태인지 확인할 수 있습니다.
수신자의 버퍼에 공간이 생기면 Window Update가 발생할 수 있습니다.
Window Update 이후 송신자는 다시 데이터를 보낼 수 있습니다.
이번 실험에서는 정상 수신 상태와 흐름제어 상태를 비교했습니다.
정상 상태에서는 Server가 데이터를 빠르게 읽었기 때문에 흐름제어 관련 이벤트가 출력되지 않았습니다.
흐름제어 상태에서는 Server가 데이터를 천천히 읽었기 때문에 Window Full과 Zero Window가 출력되었습니다.
Window Full은 Client가 Server의 수신 윈도우를 거의 다 채운 상태입니다.
Zero Window는 Server가 더 이상 받을 공간이 없다고 Client에게 알린 상태입니다.
이 흐름은 Server의 수신 버퍼에 여유 공간이 없어 Client의 전송이 제한되는 상황입니다.
따라서 이번 실험을 통해 TCP 흐름제어가 수신자의 처리 속도에 따라
송신자의 전송량을 조절한다는 것을 확인할 수 있었습니다.
혼잡제어가 네트워크를 보호하는 기능이라면 흐름제어는 수신자를 보호하는 기능입니다.
TCP는 rwnd와 cwnd를 함께 고려하면서 안정적인 데이터 전송을 수행합니다.
결국 TCP 흐름제어의 핵심은 간단합니다.
수신자가 받을 수 있으면 데이터를 보냅니다.
수신자가 받을 수 없으면 잠시 기다립니다.
수신자가 다시 받을 수 있게 되면 전송을 다시 이어갑니다.
TCP 흐름제어는 수신자의 처리 속도에 맞춰 안정적으로 데이터를 전달하기 위한 핵심 기능입니다.
'Network' 카테고리의 다른 글
| Wireshark로 실제 패킷 헤더와 16진수 덤프 확인하기 (0) | 2026.07.09 |
|---|---|
| 리눅스에서 MTU와 패킷 단편화 실험 (0) | 2026.07.05 |
| TCP 혼잡제어 (0) | 2026.06.25 |
| What is Socket with Vibe Coding (0) | 2026.06.19 |
| T-Pot을 안전하게 운영하기 위한 DMZ 네트워크 설계 (0) | 2026.06.18 |