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

비밀번호

댓글 쓴 사람
 



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

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12752정성태8/4/202143개발 환경 구성: 592. JetBrains의 IDE(예를 들어, PyCharm)에서 Visual Studio 키보드 매핑 적용
12751정성태8/4/202155개발 환경 구성: 591. Windows 10 WSL2 환경에서 docker-compose 빌드하는 방법
12750정성태8/3/202182디버깅 기술: 181. windbg - 콜 스택의 "Call Site" 오프셋 값이 가리키는 위치
12749정성태8/2/202173개발 환경 구성: 590. Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원
12748정성태8/2/202131개발 환경 구성: 589. Azure Active Directory - tenant의 관리자(admin) 계정 로그인 방법
12747정성태8/1/202146오류 유형: 748. 오류 기록 - MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER파일 다운로드1
12746정성태7/31/202199개발 환경 구성: 588. 네트워크 장비 환경을 시뮬레이션하는 Packet Tracer 프로그램 소개
12745정성태7/31/202128개발 환경 구성: 587. Azure Active Directory - tenant의 관리자 계정 로그인 방법
12744정성태7/30/202133개발 환경 구성: 586. Azure Active Directory에 연결된 App 목록을 확인하는 방법?
12743정성태7/30/202153.NET Framework: 1083. Azure Active Directory - 외부 Token Cache 저장소를 사용하는 방법파일 다운로드1
12742정성태7/30/202137개발 환경 구성: 585. Azure AD 인증을 위한 사용자 인증 유형
12741정성태7/29/202186.NET Framework: 1082. Azure Active Directory - Microsoft Graph API 호출 방법파일 다운로드1
12740정성태7/29/202156오류 유형: 747. SharePoint - InvalidOperationException 0x80131509
12739정성태7/28/202165오류 유형: 746. Azure Active Directory - IDW10106: The 'ClientId' option must be provided.
12738정성태7/28/202167오류 유형: 745. Azure Active Directory - Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
12737정성태7/28/202169오류 유형: 744. Azure Active Directory - The resource principal named api://...[client_id]... was not found in the tenant
12736정성태7/28/202150오류 유형: 743. Active Azure Directory에서 "API permissions"의 권한 설정이 "Not granted for ..."로 나오는 문제
12735정성태7/27/202186.NET Framework: 1081. C# - Azure AD 인증을 지원하는 데스크톱 애플리케이션 예제(Windows Forms)파일 다운로드1
12734정성태7/26/2021100스크립트: 20. 특정 단어로 시작하거나/끝나는 문자열을 포함/제외하는 정규 표현식 - Look-around
12733정성태7/23/2021124.NET Framework: 1081. Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면?파일 다운로드2
12732정성태7/23/202165오류 유형: 742. SharePoint - The super user account utilized by the cache is not configured.
12731정성태7/23/202184개발 환경 구성: 584. Add Internal URLs 화면에서 "Save" 버튼이 비활성화 된 경우
12730정성태7/23/202198개발 환경 구성: 583. Visual Studio Code - Go 코드에서 입력을 받는 경우
12729정성태7/22/202196.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
12728정성태7/22/202179.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...