I/O 요청을 등록하고, 디바이스 드라이버가 I/O 요청을 처리하기 이전에 앞서 요청했던 I/O 작업을 취소하고 싶을 수있다. 윈도우에서는 이를 위해 CancleIo와 CancleIoEx 함수를 제공한다.
CancleIo
BOOL WINAPI CancelIo(
_In_ HANDLE hFile
);
지정된 핸들에 대해 진행 중인 I/O작업이 있고, 해당 함수를 호출 하는 스레드에서 실행된 경우 CancleIo
함수는 이를 취소한다.
I/O작업은 Overlapped I/O로 실행될때만 유효하다. 취소된 모든 I/O작업은 ERROR_OPERATION_ABORTED를 포함하여 완료되며, I/O 작업에 대한 모든 완료통지는 정상적으로 발생한다.
해당 함수는 핸들이 IOCP와 연계되어 있는 경우 사용하기 힘들다. 따라서 멀티스레드 환경과 IOCP를 사용하여 구성하는 게임서버에서는 활용하기가 힘들다.
CancleIoEx
BOOL WINAPI CancelIoEx(
_In_ HANDLE hFile,
_In_opt_ LPOVERLAPPED lpOverlapped
);
따라서 IOCP를 활용하는 경우에는 CancleIoEx
를 사용해야한다. 해당 함수는 I/O 작업을 만든 스레드에 관계없이 해당 함수를 호출한 프로세스에서 요청한 보류 중인 모든 I/O작업에 대해 취소할 수 있다. 다른 스레드에서 요청한 I/O작업까지 취소할 수 있다는 말이된다.
lpOverlapped 파라미터에 NULL을 넣어주게 되면 해당 핸들과 연관된 모든 I/O 작업을 취소할 수 있고, 특정 오버랩을 넣어주게되면 해당 오버랩에 연관된 특정 I/O작업에 대한 요청만 취소할 수 있다.
핸들에 IOCP와 연계되어 있는 경우, 동기 작업이 취소되면 Completion Queue에 작업이 들어오지않지만, 아직 완료되지 않을 비동기 작업에 경우에는 완료통지가 온다.
취소 중인 작업은 세 가지 상태중 하나로 완료된다.
1. I/O 작업의 정상적인 완료
취소 요청이 들어가는 순간 작업이 완료될 가능성도 있으므로 이 상태가 발생할 수 있다.
2. 작업 취소
ERROR_OPERATION_ABORTED와 함꼐 완료통지가 온다.
3. 다른 오류가 발생하여 작업이 실패
GetLastError를 통해 관련 오류코드를 얻을 수 있다.
게임서버에서 활용 방안
closesocket() 중복 호출의 문제점
서버에서 recv로 0바이트를 수신하거나(클라 측에서 FIN패킷 전송한 상황), recv, send 과정에서 IO_PENDING 이외의 에러가 나오거나, 여러가지의 이유로 해당 세션의 연결을 끊을 것이고, 이 상황에서 closesocket()
을 호출하여 해당 세션에 대한 소켓을 반남할 것이다. 이 떄 멀티스레드 환경에서는 여러 스레드에서 동시에 연결을 끊으려고 시도할 수 있고, 즉 여러스레드에서 동시에 closesocket()
을 호출하는 상황이 발생할 수 있다.
이 상황에서 최초closesocket()
을 호출하여 소켓을 반납하는 순간에, 새로운 세션이 accept 되었다면, 반납한 소켓을 재사용할 수 있다. 윈도우 내부적으로 방금 반환한 소켓을 바로 재사용하도록 설계되어 있기 때문에, 방금 반납한 소켓을 재사용할 가능성이 높다. 이때 다음 closesocket()
을 호출하면, 엉뚱한 세션이 접속되자마자 종료가 되는 경우가 발생할 수 있다.
이렇기에 closesocket()
을 중복으로 호출하는 것을 막거나, 딱 한번만 호출하게 해야할 것이다.
CancleIoEx
의 활용
만약 한번만 호출되도록 설계하려면, 해당 세션에 대한 참조가 아무것도 없을 때, closesocket()
을 호출하는 방향으로 생각해볼 수 있을 것이다. 이떄, 클라 측에서 먼저 연결을 끊을 경우에는, 아무런 문제가 없다. 세션에 대한 참조가 아무것도 없다는 것은 이미 해당 소켓에 대해 어떠한 I/O도 걸려있지 않다는 것이고, 클라 측에서 먼저 연결을 끊는 경우에는 해당 세션에 대한 I/O가 잘 정리 되기 때문이다.
하지만 문제점은 악의적인 유저를 감지했거나 모종의 이유로 서버가 먼저 클라이언트와의 연결을 끊을 경우인데, 가장 먼저 생각나는 특정 상황을 설명해보자면, 현재 해당 세션에 대해 send와 recv작업이 모두 걸려있고, 딱 이 상황에서 클라이언트가 고의적으로 아무런 데이터도 보내지 않는 상황이다. 그러면 send작업은 알아서 완료가 되겠지만, 클라이언트가 아무런 데이터를 보내지 않는 이상 recv에 대한 요청은 평생 완료되지 않을 것이다. recv요청이 완료되지 않는 이상, 해당 세션에 대한 참조는 절대 0이 될 수 없으며, 이렇게 연결이 계속 유지되면 연결정보가 NP 풀에 남게될것이다. 이러한 상황이 수만개, 수십만개가 된다면, 서버의 NP풀 자원을 갉아먹을 것이고, 점점 늘어나게되면 서버 컴퓨터가 뻗어버릴 것이다.
따라서 이러한 상황에서 요청된 I/O작업을 강제로 취소시켜야하는데 이 떄 활용할 수 있는 것 중하나가CancleIoEx
이다. 해당 함수로 걸려있는 I/O요청을 강제로 취소시켜버리면 참조카운트가 0이 될 수 있고, 정상적으로 closesocket()
을 호출하여 해당 자원을 정리할 수 있게 될것이다.
참고 자료
- 제프리 리처의 WINDOWS VIA C/C++
게임개발자를 꿈꾸는 대학생의 개발 공부 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!