Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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

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

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

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

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

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

Graceful Shutdown, Linger Options, and Socket Closure
; https://docs.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://docs.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://docs.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 호출을 확인해 작성한 것입니다. ^^)




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 2/17/2021 ]

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

비밀번호

댓글 쓴 사람
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12594정성태4/11/202147.NET Framework: 1035. C# - kubectl 명령어 또는 REST API 대신 Kubernets 클라이언트 라이브러리를 통해 프로그래밍으로 접근파일 다운로드1
12593정성태4/10/202124개발 환경 구성: 567. Docker Desktop for Windows - kubectl proxy 없이 k8s 대시보드 접근 방법
12592정성태4/10/202144개발 환경 구성: 566. Docker Desktop for Windows - k8s dashboard의 Kubeconfig 로그인 및 Skip 방법
12591정성태4/9/202183.NET Framework: 1034. C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법파일 다운로드1
12590정성태4/9/202148.NET Framework: 1033. C# - .NET 4.0 이하에서 Console.IsInputRedirected 구현
12589정성태4/8/202164.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법
12588정성태4/7/202155개발 환경 구성: 565. PowerShell - New-SelfSignedCertificate를 사용해 CA 인증서 생성 및 인증서 서명 방법
12587정성태4/7/202183개발 환경 구성: 564. Windows 10 - ClickOnce 배포처럼 사용할 수 있는 MSIX 설치 파일
12586정성태4/5/202151오류 유형: 710 . Windows - Restart-Computer / shutdown 명령어 수행 시 Access is denied(E_ACCESSDENIED)
12585정성태4/5/202149개발 환경 구성: 563. 기본 생성된 kubeconfig 파일의 내용을 새롭게 생성한 인증서로 구성하는 방법
12584정성태4/5/202174개발 환경 구성: 562. kubeconfig 파일 없이 kubectl 옵션만으로 실행하는 방법
12583정성태3/29/2021129개발 환경 구성: 561. kubectl 수행 시 다른 k8s 클러스터로 접속하는 방법
12582정성태3/29/202156오류 유형: 709. Visual C++ - 컴파일 에러 error C2059: syntax error: '__stdcall'
12581정성태3/28/2021239.NET Framework: 1031. WinForm/WPF에서 Console 창을 띄워 출력하는 방법 (2) - Output 디버깅 출력을 AllocConsole로 우회 [2]
12580정성태3/28/202152오류 유형: 708. SQL Server Management Studio - Execution Timeout Expired.
12579정성태3/28/202163오류 유형: 707. 중첩 가상화(Nested Virtualization) - The virtual machine could not be started because this platform does not support nested virtualization.
12578정성태3/27/2021136개발 환경 구성: 560. Docker Desktop for Windows 기반의 Kubernetes 구성 (2) - WSL 2 인스턴스에 kind가 구성한 k8s 서비스 위치
12577정성태3/29/2021169개발 환경 구성: 559. Docker Desktop for Windows 기반의 Kubernetes 구성 - WSL 2 인스턴스에 kind 도구로 k8s 클러스터 구성
12576정성태3/25/2021164개발 환경 구성: 558. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성 (2) - k8s 서비스 위치
12575정성태3/24/2021143개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/28/2021199.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태4/1/202187개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어
12572정성태3/28/2021164.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태4/1/2021176오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태4/7/2021112개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...