포트폴리오
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인 세션 객체 등 접근
- 전혀 알 수 없는 패턴
- 완료 통지가 온 세션과 실제 찾은 세션이 다른 상황
따라서 lpOverlapped를 확장하여 디버깅
- lpOverlapped에 비동기 I/O를 요청한 세션 포인터를 저장
- GQCS의 키 값으로 반환된 세션과 비교
- pSession과 lpOverlapped->ptr이 다른 것을 확인
조사식을 통해 pSession의 값을 lpOverlapped->ptr의 값으로 바꾸고 진행
- 정상적으로 잘 수행되는 것을 확인
- 이로 재사용된 소켓에 대한 IOCP 핸들 연계시 Key값이 변경되지 않고 이전의 Key값이 반환된다는 사실을 확인
따라서 IOCP 핸들과 소켓의 연계 시점의 반환값을 확인해야 함
- 같은 소켓에 대해 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 감소를 보임
- 따라서 직전 버전의 서버로 다음 단계를 진행할 예정임
반응형