2. Nagle/TCP_NODELAY, 흐름제어 vs 혼잡제어, 패킷 경계 문제
난이도 중내 답안
모범답안
모범답안 — Nagle/TCP_NODELAY, 흐름제어 vs 혼잡제어, 패킷 경계
난이도: 중
핵심 답변
- Nagle: 작은 패킷을 모아 보내 네트워크에 작은 세그먼트가 범람하는 것을 막는 TCP 최적화. "보낼 작은 데이터가 있어도, 아직 ACK 안 받은 미확인 데이터가 있으면 모았다가 보낸다." → 지연(최대 수십~수백 ms)이 생긴다.
- 게임서버는 작은 패킷을 즉시 보내야 하므로 보통
TCP_NODELAY로 Nagle을 끈다. 트레이드오프는 작은 패킷이 많아져 대역폭/패킷 수 오버헤드 증가. - 흐름 제어: 송신자가 느린 수신자를 압도하지 않게(수신 버퍼 보호). 수신 윈도우(rwnd) 기반.
- 혼잡 제어: 네트워크 전체가 막히지 않게(라우터/링크 보호). 혼잡 윈도우(cwnd) 기반.
- TCP는 경계가 없다. 한 번의
recv가 메시지 절반만 주거나(부분 수신), 두 메시지를 합쳐 줄 수도 있다(병합). 따라서 길이 프리픽스나 구분자로 직접 프레이밍해야 한다.
깊이 있는 설명
Nagle & TCP_NODELAY
Nagle 규칙: 미확인(unacknowledged) 데이터가 남아 있으면, MSS보다 작은 데이터는 모았다가 ACK가 오거나 한 덩어리가 MSS만큼 찰 때 보낸다. 텔넷처럼 1바이트씩 보내 헤더 40바이트를 낭비하는 상황을 막으려는 것. 게임은 "키 입력 즉시 전송"이 필요하므로 이 지연이 독이다. setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, ...)로 끈다.
Nagle + Delayed ACK 상호작용(꼬리질문 2): 수신측 Delayed ACK는 "ACK를 바로 안 보내고 ~40-200ms 모았다 보내거나, 보낼 데이터에 piggyback." 그런데 송신측 Nagle은 "ACK 와야 다음 작은 패킷 보냄." → 서로 상대를 기다리는 교착 비슷한 상태가 되어 한 번 주고받는 데 수백 ms 지연. 이른바 Nagle/Delayed-ACK deadlock. 게임에서 치명적이라 TCP_NODELAY가 사실상 필수.
흐름 제어 vs 혼잡 제어
- 흐름 제어: 수신자가 TCP 헤더의 Window Size 필드(rwnd) 로 "내 버퍼에 이만큼 더 받을 수 있다"를 광고. 송신자는 그 이상 보내지 않는다. 수신 버퍼가 꽉 차면 window=0을 광고해 송신을 멈춘다. → 종단(end-to-end) 1:1 보호.
- 혼잡 제어: 네트워크 중간의 혼잡을 감지(주로 패킷 손실/지연)해 송신 속도를 조절. cwnd를 두고 Slow Start(지수 증가) → Congestion Avoidance(선형 증가) → 손실 시 cwnd 급감(Reno의 fast recovery, 혹은 CUBIC/BBR). → 네트워크 공유 자원 보호.
- 실제 송신량 =
min(rwnd, cwnd).
패킷 경계 (TCP=스트림)
질문 4 코드의 버그:
recv는 요청한 만큼이 아니라 그 순간 커널 버퍼에 있는 만큼만 반환한다. 메시지가 쪼개져 오면buf에 절반만 있고, 그걸 패킷으로 캐스팅하면 쓰레기 값/over-read.- 두 메시지가 합쳐져 오면 두 번째 메시지를 통째로 잃거나 오파싱.
recv반환값n을 검사하지 않음:0(상대 정상 종료),-1(에러/EAGAIN)을 메시지처럼 처리하면 버그.- 엔디안/패딩을 무시한 직접 캐스팅은 이식성·정렬 문제.
응용/실무 연결
길이 프리픽스 프레이밍 설계(질문 5):
- 모든 메시지를
[길이(예: 2 or 4바이트, 네트워크 바이트오더)][페이로드]형태로 보낸다. - 연결마다 수신 누적 버퍼(ring buffer 또는 자라는 버퍼) 를 둔다.
recv로 받은 바이트를 버퍼에 append. - 루프:
- 헤더(길이 필드)만큼 쌓였는가? 아니면 더 받기.
- 길이를 읽고
payload_len만큼 더 쌓였는가? 아니면 더 받기(부분 수신 처리). - 한 메시지가 완성되면 잘라서 핸들러로 넘기고, 버퍼에서 소비분 제거. 남은 바이트로 다음 메시지를 다시 검사(병합 처리).
처리해야 할 엣지 케이스:
- 헤더조차 다 안 온 경우(2바이트 중 1바이트만).
- 한
recv에 여러 메시지가 섞여 온 경우 → 루프로 다 꺼낼 때까지 반복. - 길이 필드 검증:
payload_len이 비정상적으로 크면(악의적 클라이언트) 상한선으로 끊고 연결 종료. 안 그러면 메모리 폭주/DoS. recv == 0(정상 종료),recv < 0(EAGAIN이면 다음 이벤트 대기, 그 외 에러면 종료).- non-blocking 소켓이면
send도 부분 전송 가능 → 송신 버퍼/오프셋 관리 필요.
게임서버에서는 보통 길이 프리픽스 + 패킷 ID + 페이로드 구조를 쓰고, 직렬화는 FlatBuffers/Protobuf 또는 직접 정의한 바이너리 포맷을 쓴다.
흔한 오답·함정
- "UDP도 경계 처리해야 한다" → UDP는 데이터그램 단위라 경계가 보장된다. 프레이밍 문제는 TCP 한정.
- "흐름 제어와 혼잡 제어는 같은 것" → 보호 대상이 다르다(수신자 vs 네트워크).
- "TCP_NODELAY 켜면 무조건 좋다" → 작은 패킷을 자주 보내는 비효율은 그대로. 정말 작은 데이터가 폭증하면 애플리케이션 레벨에서 묶어 보내는(batching) 게 더 낫다. (직접 모아 한 번에 send)
- "
send하면 그 길이만큼 다 보내진다" → non-blocking에서는 부분 전송 가능. 반환값 확인 필수.
꼬리질문 대비
- Q. 게임서버에서 작은 패킷 폭증을 줄이려면? A. tick 단위로 여러 이벤트를 한 패킷에 묶어 보내는 애플리케이션 배칭. TCP_NODELAY는 켜되 batching으로 패킷 수를 줄인다.
- Q. 수신 윈도우가 0이 되면 송신측은 어떻게 다시 보내나? A. Zero Window Probe(작은 탐색 패킷)를 주기적으로 보내 윈도우가 열렸는지 확인한다.
- Q. CUBIC과 BBR의 차이? A. CUBIC은 손실 기반(loss-based), BBR은 대역폭·RTT를 측정해 모델 기반으로 보내는 속도를 정한다. BBR이 버퍼블로트에 강하다.
- Q. 길이 필드를 1바이트로 하면? A. 최대 255바이트 메시지 한계. 게임 메시지는 보통 2바이트(64KB)나 4바이트로 둔다. 단, 길이 검증은 필수.