Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 8개 있습니다.)
개발 환경 구성: 533. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 리눅스 환경
; https://www.sysnet.pe.kr/2/0/12527

개발 환경 구성: 534. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 윈도우 환경
; https://www.sysnet.pe.kr/2/0/12528

개발 환경 구성: 535. Wireshark + C#으로 확인하는 TCP 통신의 MIN RTO
; https://www.sysnet.pe.kr/2/0/12529

개발 환경 구성: 536. Wireshark + C#으로 확인하는 TCP 통신의 Receive Window
; https://www.sysnet.pe.kr/2/0/12530

개발 환경 구성: 538. Wireshark + C#으로 확인하는 ReceiveBufferSize(SO_RCVBUF), SendBufferSize(SO_SNDBUF)
; https://www.sysnet.pe.kr/2/0/12532

개발 환경 구성: 539. Wireshark + C/C++로 확인하는 TCP 연결에서의 shutdown 동작
; https://www.sysnet.pe.kr/2/0/12533

개발 환경 구성: 540. Wireshark + C/C++로 확인하는 TCP 연결에서의 closesocket 동작
; https://www.sysnet.pe.kr/2/0/12534

개발 환경 구성: 541.  Wireshark로 확인하는 LSO(Large Send Offload), RSC(Receive Segment Coalescing) 옵션
; https://www.sysnet.pe.kr/2/0/12535




Wireshark + C/C++로 확인하는 TCP 연결에서의 closesocket 동작

지난 글에서 shutdown에 대해 마이크로소프트의 문서를 기반으로 알아봤는데요,

Wireshark + C++로 확인하는 TCP 연결에서의 shutdown 동작
; https://www.sysnet.pe.kr/2/0/12533

이번에는 closesocket에 대한 문서 내용을 살펴보겠습니다.

closesocket function (winsock.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-closesocket

그리고 이와 함께 다음의 문서도 함께 곁들여 설명할 것입니다.

Graceful Shutdown, Linger Options, and Socket Closure
; https://learn.microsoft.com/en-us/windows/win32/winsock/graceful-shutdown-linger-options-and-socket-closure-2




"Graceful Shutdown, Linger Options, and Socket Closure" 문서를 보면, 1) "소켓 연결을 닫는(shutting down a socket connection) 작업"과 2) "소켓을 닫는(closing a socket descriptor) 작업"을 나눠서 설명합니다.

소켓 연결을 닫는 작업은, "shutdown sequence"라고 불리며 지난 글에서 다룬 shutdown(및 WSASendDisconnect)의 기능이라고 보면 됩니다. 즉, 상대와의 연결을 닫는 FIN 신호나 RST 신호를 shutdown 함수에서 처리하는데 각각 다음과 같은 차이점이 있었고,

  • 정상(graceful): any data that has been queued, but not yet transmitted can be sent prior to the connection being closed.
  • 비정상(abortive/hard): any unsent data is lost

위의 경우가 어떻게 발생하는지는 (또는 shutdown 호출임에도 발생하지 않는 경우도) 지난 글에서 이미 충분히 다뤘습니다.

반면 "소켓을 닫는 작업"은 소켓 핸들socket descriptor을 해제해 더 이상 해당 핸들을 재사용하지 못하도록 막는 것으로 closesocket의 고유 기능입니다. 그래서 이를 정리하면 다음과 같이 분류할 수 있습니다.

  • socket 함수: 소켓 자원 생성
  • connect 함수: 대상과 연결(3-way handshake)
  • shutdown 함수: 대상과 연결 해제(4-way handshake or RST)
  • closesocket 함수: 소켓 자원 해제

그런데, 이제부터가 약간 혼란스러워지는 계기가 되는데요, 바로 closesocket 함수가 호출되었을 때 내부적으로 소켓 상태가 shutdown sequence를 개시하지 않았다면 암시적으로 그 작업을 수행한다는 것입니다. 사실, shutdown 함수가 FIN/RST 처리를 하지 않는 경우도 있으므로 closesocket에서 shutdown sequence 단계를 고려하지 않을 수가 없습니다. (개인적으로는 이렇게 shutdown sequence가 shutdown 함수와 closesocket 내에 산재하고 있어 혼란을 더 가중시킨 듯하고, 따라서 이에 대한 이해를 어렵게 만든 주범이라고 생각합니다.)




점입가경인 것은, 안 그래도 이미 shutdown과 closesocket의 연결 종료 절차가 복잡한데, 이에 더해 closesocket의 암시적인 shutdown sequence에 대한 제어를 SO_LINGER / SO_DONTLINGER 옵션으로 제공한다는 점입니다. 그래서, 암시적인 shutdown sequence라고 해도 다시 3가지의 동작 방식으로 나뉘게 됩니다.

  • 비정상abortive 종료 - closesocket은 (송/수신 버퍼에 관계없이) 연결을 reset하고 제어는 곧바로 반환
  • 일정 시간 대기 종료 - 지정한 시간 내에 송신 버퍼를 비울 수 있으면 FIN으로 정상graceful 종료를 시도하고, 그렇지 않은 경우 RST로 비정상abortive 종료, 하지만 이때 수신 버퍼가 송신 버퍼를 비울 때까지도 내용이 있으면 RST
  • (기본값) 대기 없는 종료 - closesocket의 기본 동작으로, shutdown sequence를 개시하도록 TCP layer에 전달하고 제어는 바로 반환. 비록 정상 종료를 시도하지만 언제쯤 완료될지, 실제로 정상 종료될지 여부는 응용 프로그램 입장에서 알 수 없음.

위에서 마지막 "(기본값) 대기 없는 종료" 방식은 shutdown 글에서 다뤘던 SD_BOTH 동작을 의미하고, SO_LINGER / SO_DONTLINGER 옵션을 별도로 설정하지 않은 기본값에 해당합니다. 이에 대해서는 다음의 코드로 확인할 수 있습니다.

// LINGER structure (winsock.h)
// https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-linger

{
    int value = 0;
    int valueLen = 1;
    getsockopt(clntSocket, SOL_SOCKET, SO_DONTLINGER, (char*)&value, &valueLen);

    linger lingerOpt = { 0 };
    int lingerLen = sizeof(linger);
    getsockopt(clntSocket, SOL_SOCKET, SO_LINGER, (char*)&lingerOpt, &lingerLen);
    printf("SO_DONGLINGER: %d, l_onoff: %d, l_linger: %d\n", value, lingerOpt.l_onoff, lingerOpt.l_linger);
}

/* 출력 결과
SO_DONGLINGER: 1, onoff: 0, linger: 0
*/

그리고, SO_DONGLINGER의 값과 SO_LINGER의 onoff 값은 서로 배타적으로 같은 값입니다. 즉, SO_DONGLINGER = 0이면 linger.onoff는 1이고, 그 반대의 경우도 마찬가지입니다. 따라서 옵션 설정은 linger 하나로 제어해도 무방합니다.

linger 옵션의 기본값이 "l_onoff: 0, l_linger: 0"라는 것은 이렇게 설정되었을 때 closesocket이 "(기본값) 대기 없는 종료" 모드로 동작한다는 것을 의미합니다.

만약 closesocket 호출 시 무조건 비정상abortive 종료로 처리하고 싶다면 아래와 같이 ""l_onoff: 1, l_linger: 0"" 옵션 설정을 해,

{
    linger lingerOpt = { 0 };
    lingerOpt.l_onoff = 1;
    int lingerLen = sizeof(linger);
    setsockopt(clntSocket, SOL_SOCKET, SO_LINGER, (char*)&lingerOpt, lingerLen);
}

closesocket을 호출하면 서버와의 연결을 (송/수신 버퍼의 상태에 무관하게) RST 종료합니다. (그래도 당연히 고유 업무인 소켓 자원은 모두 해제를 합니다.)

마지막으로 일정 시간 대기 종료는 linger.l_linger 필드에 시간 값(단위: 초)을 설정해 선택할 수 있습니다.

{
    linger lingerOpt = { 0 };
    lingerOpt.l_onoff = 1;
    lingerOpt.l_linger = 1; // 단위: 초
    int lingerLen = sizeof(linger);
    setsockopt(clntSocket, SOL_SOCKET, SO_LINGER, (char*)&lingerOpt, lingerLen);
}

따라서 위의 코드는 closesocket 호출 후 1초 동안 blocking이 되고 그 시간 내에 송신이 모두 완료되면 FIN 신호graceful가 전달되고, 그렇지 않으면 RST 신호abortive가 전달됩니다. 이때 주의할 것은, 이 대기 시간에 수신 버퍼는 고려하지 않는다는 점입니다. 따라서, 송신 버퍼에 내용이 있으면 지정된 시간 동안 대기를 하지만, 그때까지도 수신 버퍼에 내용이 있으면 연결을 RST합니다. 반면 송신 버퍼에 내용이 없고 수신 버퍼에 내용이 있는 상태라면 대기 시간 없이 바로 RST합니다.




그런데, LINGER 구조체 문서를 보면 다소 혼란스러운 설명이 나옵니다.

LINGER structure (winsock.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-linger

The l_onoff member of the linger structure determines whether a socket should remain open for a specified amount of time after a closesocket function call to enable queued data to be sent.


위의 문장을 얼핏 보면 l_onoff가 closesocket 호출 후에, 즉 closesocket으로부터 제어가 반환되고 TCP layer 상에서 open 상태로 머무르는 것처럼 해석할 수 있는데요, (사실 closesocket의 암시적 호출의 기본 shutdown sequence가 그런 식으로 제어 반환 후 TCP layer 상에서 동작합니다.) 테스트를 해보면 l_onoff 시간 동안 blocking이 되는 것을 확인할 수 있습니다. (물론, 최대 대기 시간이 그렇다는 것이고 그 시간 내에 버퍼가 비워지면 곧바로 이후의 작업을 수행하고 제어를 반환합니다.)

또 한가지 이상한 점은, "to enable queued data to be sent"라고 해서 마치 송신 버퍼만 비워지면 정상graceful 종료를 할 수 있을 것처럼 설명하는데요, 이미 이에 대해 언급했지만, 예를 들어 서버에서 1 바이트를 송신하고, 클라이언트가 그 데이터를 recv로 받지 않은 상태에서 "l_onoff: 1, l_linger: 3" 옵션 설정이 되었다면, 수신 버퍼가 비어 있지 않기 때문에 (3초 동안 대기도 없이) RST 신호를 보내며 비정상 종료abortive 처리합니다.




참고로, 지난 shutdown 글에서 정상graceful 종료를 위한 시나리오를 2개 설명했는데요, 이야기만 약간 다르게 Graceful Shutdown, Linger Options, and Socket Closure 글에서도 이에 대한 시나리오를 하나 더 소개합니다.

1) 클라이언트 측: shutdown(SD_SEND) 호출
                                                2) 서버 측: 클라이언트 측의 shutdown(SD_SEND) 호출로 인해 FIN 수신
                                                           (이와 함께 recv로 모든 수신 버퍼를 비워야 함)
                                                3) 서버 측: 전송해야 할 데이터를 모두 send 처리
                                                4) 서버 측: shutdown(SD_SEND) 호출
5) 클라이언트 측: recv로 서버가 전송하는 데이터 모두 수신
  또한 서버 측의 shutdown(SD_SEND) 호출로 인해 FIN 수신

6)                  서버 및 클라이언트 측 모두 closesocket 호출

사실, shutdown의 동작을 이해한다면 저 과정이 약간의 문장 추가에 불과하다는 것을 아실 것입니다. 즉, 지난 글에 소개한 2가지와 비교해 별반 차이가 없이 수긍할 수 있는 과정입니다.




자, 그럼 이쯤에서 정리를 해볼까요?

  1. 수신 큐를 비우는 것이 매우 중요합니다. 그렇지 않으면 (특별한 경우를 제외하고는) 무조건 연결은 RESET되어 그 즉시 RST 패킷 전송으로 비정상 종료를 해버립니다.
  2. 송/수신에 상관없이 무조건 강제 연결 종료를 "l_onoff: 1, l_linger: 0" 옵션 설정 후 closesocket 호출로 할 수 있습니다.
  3. closesocket 호출만으로 암시적 shutdown sequence를 하는 경우, 해당 연결은 정상 종료할 수도 있고, 비정상 종료를 할 수도 있으며 그 결과는 closesocket의 제어 반환 이후에 나타나므로 응용 프로그램 입장에서는 알 수 없습니다. 즉, 중요한 데이터 통신이라면 반드시 4-way handshake가 발생하는 gracful shutdown을 유도해 그 결과를 확인해야 합니다.
  4. 송신 버퍼의 비우기에 대한 timeout을 설정하고 싶다면 "l_onoff: 1, l_linger: 시간(sec)"을 설정해 줍니다.

마지막으로, 제가 설명을 할 때 graceful에 대해 FIN 신호 처리로 언급했지만, 사실 소켓 통신의 특성상 양방향 커뮤니케이션이기 때문에 한 쪽에서 FIN 처리를 했다고 해서 graceful 종료가 되는 것은 아닙니다. 즉, 한쪽에서의 FIN 신호는 half-close로서만 의미가 있고 상대 측에서도 FIN 신호로 절차를 마무리해 4-way handshake 과정이 완료되어야 비로소 graceful 종료가 됩니다.

(어쩌다 보니, 제목과는 달리 wireshark를 다루지 않았는데 위의 내용은 모두 wireshark로 Windows 10 환경에서 FIN/RST 호출을 확인해 작성한 것입니다. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/9/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-05-15 11시41분
정성태

1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13422정성태10/5/20233203닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233239닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235308스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233092스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233755닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233336닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233152오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233639닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233390디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233581닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236851닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233363Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20234861닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233721닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233714Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233473닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233411VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20233837닷넷: 2138. C# - async 메서드 호출 원칙
13404정성태8/29/20233359오류 유형: 876. Windows - 키보드의 등호(=, Equals sign) 키가 눌리지 않는 경우
13403정성태8/21/20233187오류 유형: 875. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF
13402정성태8/20/20233245닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233508닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233344오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233371닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234131스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
13397정성태7/23/20233639닷넷: 2134. C# - 문자열 연결 시 string.Create를 이용한 GC 할당 최소화
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...