28. 연결 종료 시 graceful half-close 부재로 인한 송신 데이터 절단(RST) (C#)
난이도 중내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 연결 종료 시 graceful half-close 부재로 인한 송신 데이터 절단(RST) (C#)
난이도: 중상
요약
마지막 ack 를 await SendAllAsync 로 보낸 직후 Socket.Dispose() 한다. 그러나 (1) await 완료는 소켓 송신 버퍼에 복사됐다는 뜻일 뿐 상대 수신 보장이 아니며, (2) 소켓 수신 버퍼에 읽지 않은 인바운드(파이프라이닝된 다음 요청)가 남은 채 Dispose/Close 하면 커널이 RST 를 보내 미ACK 송신분(=방금 보낸 ack)까지 폐기한다.
문제점
- [조기 종료 / 전달 미보장] (A)
CloseSession()의Sock.Dispose();- 증상:
Dispose()는 기본LingerState(off)에서 가능한 한 보내려 하지만, 수신 버퍼에 미독 데이터가 있으면 RST 로 끊긴다. await 완료를 "전달 완료"로 오해. - 근본원인:
Socket.Shutdown(SocketShutdown.Send)후 상대 FIN 확인이라는 half-close 절차 부재.
- 증상:
- [미드레인 → RST] (B)
OnReadableAsync에서CloseSession()호출- 증상: 클라가
Logout과 다음 패킷을 함께 보냈다면 그 바이트가 수신 버퍼에 남고, 이 상태의Close/Dispose는 RST 를 유발 → ack 절단. .NET 에서도 동일한 TCP 의미. - 재현: 클라가
Logout+ 추가 패킷을 한 번에 전송 → 서버가 Logout 만 처리 후 즉시 Dispose → 잔여 수신 데이터로 RST → 클라 ack 수신 실패. - 근본원인: 종료 전 인바운드를 EOF까지 드레인하지 않음.
- 증상: 클라가
수정안
보낼 걸 다 보낸 뒤 Shutdown(Send) → 상대 FIN(Receive==0)까지 드레인(타임아웃 상한) → Close/Dispose.
public async Task GracefulCloseAsync(Socket sock)
{
try
{
sock.Shutdown(SocketShutdown.Send); // 우리 쪽 FIN: "더 보낼 것 없음"
// 남은 인바운드를 EOF/타임아웃까지 비운다 — RST 유발 방지
sock.ReceiveTimeout = 300; // 동기 경로면; 비동기면 CTS 사용
var buf = new byte[1024];
while (true)
{
int n;
try { n = await sock.ReceiveAsync(buf, SocketFlags.None); }
catch (SocketException) { break; } // 타임아웃/오류 시 중단
if (n == 0) break; // 상대 FIN = 정상 종료 합의
}
}
catch (SocketException) { /* 이미 끊김 */ }
finally
{
sock.Close(); // 또는 Dispose — 이제 안전
}
}
- 즉시 강제 종료(악성 클라)는
sock.LingerState = new LingerOption(true, 0);로 의도적 RST(자원 즉시 회수) — 단 마지막 데이터 전달은 포기. - 비동기 경로에선
ReceiveTimeout대신CancellationTokenSource(300ms)로 드레인 상한.
더 나은 설계
- 앱 레벨 종료 핸드셰이크: ack 에 "BYE" 를 싣고 클라의 BYE-ACK/연결종료로 확정. half-close 는 도달까지만 보장, 처리까지는 앱이 보장.
- 종료 상태 머신:
Active → Draining → Closed. Draining 중 신규 요청 무시·수신만 비움.session_network/problem10(graceful shutdown)·problem4(비동기 종료 순서)와 연결. - 드레인 상한 필수: 느린/악성 클라가 종료를 붙잡지 못하게 수백 ms 타임아웃 후 RST.
Dispose의 함정:using/Dispose패턴이 "즉시 닫음"이라 우아한 종료와 충돌. 종료 경로는 명시적 half-close 후에만 Dispose.
면접 포인트
- await 송신 완료가 "전달"이 아닌 이유, 미독 수신 데이터가 RST 를 유발하는 메커니즘.
Socket.Shutdown(Send)+ 드레인 +Close의 순서와LingerOption의 의미.- TCP half-close 가 보장하는 것(도달) vs 못 하는 것(상대 처리) — 앱 ack 가 필요한 경우.
해설 · C++
해설 — 연결 종료 시 graceful half-close 부재로 인한 송신 데이터 절단(RST) (C++)
난이도: 중상
요약
마지막 ack 를 sendAll 로 "보낸" 직후 곧바로 close() 한다. 그러나 (1) send 성공은 커널 송신 버퍼에 복사됐다는 뜻일 뿐 상대가 받았다는 보장이 아니며, (2) 소켓 수신 버퍼에 읽지 않은 인바운드 바이트(클라가 파이프라이닝한 추가 요청)가 남은 채 close() 하면 커널이 FIN 대신 RST 를 보내, 아직 ACK 안 된 송신 데이터(=방금 보낸 ack)까지 폐기된다. 결과적으로 클라는 정산 결과/종료 사유를 못 받는다.
문제점
- [조기 종료 / 전달 미보장] (A)
closeSession()의::close(s.fd)- 증상:
flushSendQueue로 커널 버퍼까지 밀어넣었어도, 아직 상대에게 전송·ACK 되기 전이다. 이 시점에 RST 가 발생하면 미전송분이 사라진다. - 근본원인:
send완료를 "전달 완료"로 오해. half-close(shutdown(SHUT_WR)) 후 상대의 FIN 까지 확인하는 절차가 없다.
- 증상:
- [미드레인 → RST] (B)
onReadable에서closeSession()호출- 증상: 클라가 같은 연결로 다음 요청을 이미 보냈다면, 그 바이트가 서버 수신 버퍼에 미독(unread) 상태로 남는다. 이 상태로
close()하면 TCP 규약상 커널은 RST 를 송신(우아한 FIN 아님)하고, RST 는 송신 큐의 미ACK 데이터를 버린다 → 방금 보낸 ack 절단. - 재현: 클라가
LOGOUT+ 곧이어 다른 패킷을 한 번에 전송 → 서버가 LOGOUT 만 처리하고 즉시 close → 수신 버퍼에 남은 패킷 때문에 RST → 클라는 ack 수신 실패. - 근본원인: 종료 전에 남은 인바운드를 드레인(EOF까지 읽기) 하지 않음.
- 증상: 클라가 같은 연결로 다음 요청을 이미 보냈다면, 그 바이트가 서버 수신 버퍼에 미독(unread) 상태로 남는다. 이 상태로
수정안
half-close: 보낼 걸 다 보낸 뒤 shutdown(SHUT_WR) 로 FIN 을 보내고, 상대의 FIN(recv==0)이 올 때까지(또는 짧은 타임아웃) 인바운드를 드레인한 다음 close.
#include <sys/socket.h>
#include <unistd.h>
void gracefulClose(Session& s) {
flushSendQueue(s); // 앱 큐 → 커널
::shutdown(s.fd, SHUT_WR); // 우리 쪽 FIN: "더 보낼 것 없음"
// 남은 인바운드를 EOF(또는 타임아웃)까지 비운다 — RST 유발 방지
char tmp[1024];
for (;;) {
ssize_t n = ::recv(s.fd, tmp, sizeof(tmp), 0);
if (n == 0) break; // 상대 FIN 수신 = 정상 종료 합의
if (n < 0) break; // EAGAIN(타임아웃)/오류 시 중단
}
::close(s.fd); // 이제 안전하게 닫음
}
- 드레인이 무한정 매달리지 않도록
SO_RCVTIMEO(또는 논블로킹+짧은 타이머)로 상한을 둔다. - 정말 즉시 끊어야 하면(악성 클라)
SO_LINGER{on,0}로 의도적으로 RST(자원 즉시 회수) — 단, 마지막 데이터 전달은 포기.
더 나은 설계
- 앱 레벨 종료 핸드셰이크: ack 에 "BYE" 의미를 싣고, 클라가 BYE-ACK 또는 연결 종료로 응답하게 하면 전달을 애플리케이션이 보장. TCP half-close 만으로는 "상대가 내 ack 를 처리했다"까지는 보장 못 함(도달까지만).
- 드레인 타임아웃 정책: 느린/악성 클라가 드레인을 붙잡지 못하게 수백 ms 상한. 초과 시
SO_LINGERRST 로 강제 회수. - 종료도 상태 머신으로:
ACTIVE → DRAINING → CLOSED. DRAINING 동안 신규 요청은 무시하되 수신만 비운다.session_network/problem10(graceful shutdown)·problem4(비동기 종료 순서)와 연결. - half-open 감지와 구분: 여기서의 RST 절단은 "정상 종료를 잘못해서" 생긴 것 —
problem9(half-open: 경로 장애)와 원인이 다름.
면접 포인트
send()성공이 "전달"이 아니라 "커널 버퍼 복사"인 이유, 그리고 close 시 미독 수신 데이터가 왜 RST 를 유발하는가.- graceful close 4단계(보낼 것 다 보냄 →
shutdown(WR)→ 드레인 →close)와SO_LINGER의 의미. - TCP half-close 가 보장하는 것(도달)과 보장 못 하는 것(상대의 처리) — 앱 레벨 ack 가 필요한 경우.