43. 신뢰성 UDP의 ACK 의미 불일치로 인한 유실 패킷의 조기 폐기
난이도 중내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 신뢰성 UDP의 ACK 의미 불일치로 인한 유실 패킷의 조기 폐기
난이도: 중
요약
수신 측은 (A) 에서 "가장 높이 본 seq(_highestSeen)" 로 ACK 를 보내는데, 송신 측은 (B) 에서
그 값을 누적 ACK(cumulative ACK) 로 해석해 "ackSeq 이하 전부 확인됨" 으로 처리한다. 두 쪽의
ACK 의미가 어긋나, 갭 너머의 seq 를 ACK 하는 순간 아직 전달되지 않은 유실 패킷이 재전송 대상에서
제거되어 영영 전달되지 않는다.
문제점
- [프로토콜 의미 불일치] 누적 vs 최고수신 혼동: 누적 ACK 는 "이 값까지 연속으로(빈칸 없이)
받았다" 를 뜻해야 한다. 그런데
(A)는 연속 여부와 무관하게_highestSeen을 보낸다. - 재현(seq=5 유실, 6·7 도착):
- 6 도착 →
_reorder={6},_highestSeen=6,_expected=5그대로 →SendAck(6). - 7 도착 →
_reorder={6,7},_highestSeen=7→SendAck(7). - 송신 측
OnAck(7)→_unacked에서 5,6,7 모두k <= 7이라 제거. - 유실된 seq=5 가 재전송 버퍼에서 사라졌으므로
OnRetransmitTick이 다시 보내지 않는다. - 수신 측은
_expected=5에서 영원히 막혀 6,7 도 상위로 전달하지 못한다. → 데이터 영구 유실 + 전달 정지(head-of-line 영구 정지).
- 6 도착 →
- 근본 원인: ACK 는 "마지막 연속 수신 지점(
_expected-1)" 을 실어야 누적 제거와 정합한다._highestSeen은 갭을 건너뛴 낙관적 값이라 누적 의미와 충돌한다.
수정안
누적 ACK 로 통일한다. 수신 측이 "마지막 연속 수신 seq" 를 ACK 하고, 송신 측은 그 값까지만 제거한다. (선택적 확인이 필요하면 SACK 비트맵을 별도로 전달한다.)
// 수신 측: 연속 수신 지점을 ACK
public void OnData(uint seq, byte[] payload)
{
uint cumAck() => _expected - 1; // _expected==0 이면 "아직 없음" 을 표현할 값 규약 필요
if (seq < _expected) { _link.SendAck(cumAck()); return; }
_reorder[seq] = payload;
while (_reorder.TryGetValue(_expected, out var p))
{ _app.Deliver(p); _reorder.Remove(_expected); _expected++; }
_link.SendAck(cumAck()); // 갭이 있으면 갭 이전까지만 ACK → 유실분은 계속 재전송됨
}
_expected 를 "다음에 기다리는 seq" 로 두면, 갭이 있는 한 누적 ACK 는 갭 이전에 머물고, 송신 측
OnAck 의 k <= ackSeq 제거는 유실 패킷(seq=5)을 건드리지 않으므로 타이머에 의해 재전송된다.
더 나은 설계
- 누적 ACK + 선택적 ACK(SACK): 누적 지점 외에 "그 뒤로 개별 수신한 seq" 를 비트맵으로 함께 보내면, 송신 측이 유실분만 콕 집어 빠르게 재전송(fast retransmit)할 수 있어 갭 복구가 빨라진다.
- seq 랩어라운드:
uint비교k <= ackSeq는 시퀀스가 한 바퀴 돌면 오작동한다. 시리얼 산술 (RFC1982식 부호 비교)로 판정해야 한다. - 버퍼 상한: 재정렬 버퍼·미확인 버퍼에 크기 상한을 둬 갭이 오래 지속될 때 무한 증가를 막는다.
- 트레이드오프: 순수 누적 ACK 는 단순하지만 갭 복구가 느리고(타임아웃 대기), SACK 는 빠르지만 구현·대역폭 오버헤드가 있다.
면접 포인트
- 누적 ACK 의 정확한 의미("연속 수신 지점")와, 이를 어기면 왜 데이터가 유실되는가.
- 누적 ACK vs 선택적 ACK(SACK)의 복구 속도·복잡도 트레이드오프(TCP 의 SACK 옵션과 대응).
- 시퀀스 번호 비교에서 랩어라운드를 시리얼 산술로 다뤄야 하는 이유.
해설 · C++
해설 — RUDP ACK 의미 불일치 + 확인 버퍼 순회 중 삭제(반복자 무효화)
난이도: 중상
요약
두 개의 결함이 겹친다. (1) 수신 측 (A) 가 "가장 높이 본 seq" 로 ACK 하는데 송신 측 (B) 는 이를
누적 ACK 로 해석해 갭 너머까지 확인 처리 → 유실 패킷이 재전송에서 사라진다. (2) (B) 의 제거 루프가
std::map 을 순회하면서 erase(it) 후 ++it 를 해 무효화된 반복자를 증가시키는 미정의 동작이다.
문제점
- [프로토콜 의미 불일치] 누적 vs 최고수신: 누적 ACK 는 "빈칸 없이 연속으로 여기까지 받음" 을
뜻해야 한다.
(A)의highestSeen_은 갭을 건너뛴 낙관값이라 누적 제거와 충돌한다.- 재현(seq=5 유실, 6·7 도착): 수신 측이
sendAck(7)→ 송신 측onAck(7)이 5,6,7 을 모두 제거 → seq=5 는 재전송되지 않고, 수신 측expected_=5에서 영구 정지. 데이터 유실 + 전달 정지.
- 재현(seq=5 유실, 6·7 도착): 수신 측이
- [반복자 무효화 / UB] 순회 중 삭제:
std::map::erase(it)는it를 무효화한다. 그런데 루프는 그 뒤++it를 수행하므로 무효 반복자를 증가시키는 미정의 동작이다(크래시·무한 루프·메모리 손상).erase의 반환값(다음 반복자)을 받아야 한다. - 재현 조건: 손실이 있는 일반적 네트워크. UB 는 두 개 이상 확인 제거가 일어날 때 바로 드러난다.
수정안
누적 ACK 로 의미를 통일하고, 삭제는 erase 반환 반복자로 안전하게 진행한다.
// 송신 측: 순회 중 안전한 삭제 + (누적) 하한 이하 제거
void onAck(uint32_t ackSeq) {
for (auto it = unacked_.begin(); it != unacked_.end(); ) {
if (it->first <= ackSeq) it = unacked_.erase(it); // 반환 반복자로 진행
else ++it;
}
// map 은 정렬되어 있으므로 upper_bound 로 구간 삭제하면 더 간결·빠르다:
// unacked_.erase(unacked_.begin(), unacked_.upper_bound(ackSeq));
}
// 수신 측: 연속 수신 지점을 ACK (갭이 있으면 갭 이전까지만)
void onData(uint32_t seq, const std::vector<uint8_t>& payload) {
if (seq < expected_) { link_.sendAck(expected_ - 1); return; }
reorder_[seq] = payload;
for (auto it = reorder_.find(expected_); it != reorder_.end() && it->first == expected_; ) {
app_.deliver(it->second);
it = reorder_.erase(it);
++expected_;
}
link_.sendAck(expected_ - 1); // expected_==0 표현은 별도 "아직 없음" 규약 필요
}
더 나은 설계
- 누적 ACK + SACK 비트맵: 유실분만 골라 빠르게 재전송(fast retransmit)해 갭 복구를 앞당긴다.
- 정렬 컨테이너 활용:
std::map은 키 정렬을 보장하므로erase(begin, upper_bound(ack))로 누적 제거를 O(log n + k) 에 안전하게 처리 — 수동 순회보다 실수 여지가 적다. - seq 랩어라운드:
<=대신 시리얼 산술 비교로 한 바퀴 도는 시퀀스를 처리한다. - 버퍼 상한: 재정렬·미확인 버퍼에 상한을 둬 장기 갭에서의 무한 증가를 막는다.
- 트레이드오프: 누적 전용은 단순하나 복구가 느리고, SACK 는 빠르나 대역폭·구현 비용이 있다.
면접 포인트
- 누적 ACK 의 정확한 의미와, 최고수신 값을 ACK 로 쓰면 유실이 어떻게 은폐되는가.
std::map/set에서 순회 중erase의 반복자 무효화 규칙과erase반환값·구간 삭제 관용구.- 누적 ACK vs SACK 의 복구 속도·복잡도 트레이드오프.