포트폴리오

IOCP Echo Server 09 - Socket Pool

묭묭.cpp 2024. 11. 14. 19:33

IOCP Echo Server 09 - Socket Pool

목적

소켓 생성 비용을 아껴보자

  • closesocket을 하지 않고 다른 방식으로 연결을 끊고 소켓을 재사용
  • socket() 부하는 줄일 수 있지 않을까?

소켓을 재사용하는 방법

TransmitFile 혹은 DisconnectEx 함수

TransmitFile

BOOL TransmitFile(
  SOCKET                  hSocket,
  HANDLE                  hFile,
  DWORD                   nNumberOfBytesToWrite,
  DWORD                   nNumberOfBytesPerSend,
  LPOVERLAPPED            lpOverlapped,
  LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
  DWORD                   dwReserved
);
  • 데이터를 전송하는 함수임
  • 근데 전송 후 동작을 설정할 수 있음
  • dwReserved : 옵션 전달 매개변수
    • TF_DISCONNECT | TF_REUSE_SOCKET 조합으로 전달하면 연결을 끊고 소켓을 재사용할 수 있음
  • lpOverlapped을 전달하여 비동기 I/O로 동작시킬 수 있음

DisconnectEx

BOOL DisconnectEx(
  _In_ SOCKET       hSocket,
  _In_ LPOVERLAPPED lpOverlapped,
  _In_ DWORD        dwFlags,
  _In_ DWORD        reserved
);
  • TransmitFile(TF_DISCONNECT)과 동일한 동작을 함
  • dwFlags 옵션에 TF_REUSE_SOCKET을 전달하여 소켓 재사용 가능
  • reserved는 항상 0으로 전달

TCP TIME_OUT 상태 때문에 비동기 I/O로 동작시킬 것

구현

CSocketPool

먼저 socket() 함수로 생성된 소켓을 관리할 소켓 풀 클래스를 구현함

class CSocketPool
{
public:
    static BOOL Init(int poolSize);
    static SOCKET Alloc();
    static void Free(SOCKET delSock);

private:
    inline static std::vector<SOCKET>        m_arrSocketPool;
    inline static SRWLOCK                    m_Lock;
};

Init()

  • 서버 초기화 과정에서 미리 호출하여 원하는 갯수의 소켓을 준비

Alloc()

  • 소켓이 있다면 있는 것을 반환
  • 없으면 새로 할당해서 반환

Free()

  • 소켓 풀에 반환

모든 동작은 SRWLock으로 동기화

PostDisconnectEx

BOOL CLanServer::PostDisconnectEx(CSession *pSession);
  • DisconnectEx 함수 또한 mswsock.lib의 함수이므로 함수 포인터를 얻어와 사용해야 함
  • OverlappedEx 구조체의 type을 IOOperation::DISCONNECTEX 설정하고 DisconnectEx 호출

이 함수가 ReleaseSession 함수에서 closesocket 대신 호출됨

  • 딱 여기까지 수행하고 뒤 socket 반환과 세션 삭제의 동작은 완료 루틴에서 처리

DisconnectExCompleted

BOOL CLanServer::DisconnectExCompleted(CSession *pSession);
  • 실제 세션 해제와 소켓 반환을 수행

문제점

여기까지 구현하고 코드에 적용 시키면 잘 작동할 줄 알았으나...

  • 문제가 발생하여 추적 시작
  • 전혀 엉뚱한 세션 포인터(키값)에 대한 완료 통지가 도착하기 시작함

이미 삭제된 세션 객체 혹은 IoCount가 0인 세션 객체 등 접근

  • 전혀 알 수 없는 패턴

test29

  • 완료 통지가 온 세션과 실제 찾은 세션이 다른 상황

따라서 lpOverlapped를 확장하여 디버깅

  • lpOverlapped에 비동기 I/O를 요청한 세션 포인터를 저장
  • GQCS의 키 값으로 반환된 세션과 비교

test30

  • pSession과 lpOverlapped->ptr이 다른 것을 확인

조사식을 통해 pSession의 값을 lpOverlapped->ptr의 값으로 바꾸고 진행

  • 정상적으로 잘 수행되는 것을 확인
  • 이로 재사용된 소켓에 대한 IOCP 핸들 연계시 Key값이 변경되지 않고 이전의 Key값이 반환된다는 사실을 확인

따라서 IOCP 핸들과 소켓의 연계 시점의 반환값을 확인해야 함

test31

  • 같은 소켓에 대해 2번 이상 IOCP 연계 작업을 진행하면 CreateIoCompletionPort 함수가 실패함

즉, 소켓을 재사용하면 key값으로 세션 포인터를 사용하지 못함

해결

OVERLAPPED 구조체를 확장한 OverlappedEx 구조체에 포인터 멤버를 받음

  • GQCS가 반환되었을 때 이것을 믿고 pSession으로 캐스팅하여 사용
  • 에코 서버가 정상적으로 동작하는 것을 확인 가능
    • 에코 서버의 경우에는 그러함
    • 더 복잡한 서버가 될 경우에도 정상 동작할지는 잘 모르겠음

추가로 서버에 올려서 테스트하며 TIME_WAIT 상태를 관찰

  • 관련 옵션은 건드리지 않음
  • netstat -ap tcp | find "TIME_WAIT" 명령어
    • 해당 명령어로 TIME_WAIT 상태에 남는 연결을 체크해본 경우
    • 이런 상황은 없는 것으로 확인
  • TransmitFile 혹은 DisconnectEx 함수는 Linger 옵션이 적용된 RST 패킷으로 연결을 끊는 것이 아닌 FIN 으로 우아한 종료를 시도하기 때문에 TIME_WAIT 상태에 빠지는 줄 알았으나 그렇지 않다는 것을 확인함
    • 확실한지는 모르겠음.

직전에 진행한 것보다 성능이 더 좋아지지는 않았음

  • 전체적인 TPS 감소를 보임
  • 따라서 직전 버전의 서버로 다음 단계를 진행할 예정임
반응형