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)
13598정성태4/16/2024212닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024294닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024511닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024528닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/2024758닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/2024948닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241186C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241155닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241070Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241132닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241185닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241135오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241265Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241089Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241044개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241145Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241218Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241366개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241131닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241493오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241620닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241850닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241539닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241662닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20241552닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...