이전 포스팅에서 Overlapped 구조체를 사용해서
비동기 파일 입출력을 구현했었다.
WriteFile함수와 ReadFile 함수에 Overlapped 구조체를 넣고 IO를 중첩시키고,
WSASend 하면 바로 리턴됨 (ERROR_IO_PENDING)
GetOverlappedResult함수로 해당 IO의 결과를 얻을 수 있게 해주는 함수도 있었다.
IOCP는 Overlapped IO로 바탕으로 결과를 받아 처리하는 하나의 방식으로
Overlapped IO를 기본적으로 알고 있어야 한다.
IO가 여러개 중첩되는 것, 요청하고 기다릴 필요없이 리턴하고
다른 일을 수행하는 것이 Overlapped IO이고,
이 작업 완료 여부를 체크하는 방법 중 하나가 IOCP인 것이다.
Overlapped구조체, 이벤트 객체멤버 등 다른 방법들이 있지만
IOCP는 입출력 요청한 스레드는 계속 다른 작업이 가능하다.
IOCP 개요
IOCP는 윈도우에서 제공하는 일반적인 I/O 모델 중 최고 성능을 갖고 있다 한다.
커널 객체(운영체제의 핵심적인 기능 프로세스, 스레드 메모리 관리 기능 제공하는 객체)이다.
https://docs.microsoft.com/ko-kr/windows/win32/sysinfo/kernel-objects
커널 개체 - Win32 apps
커널 개체 핸들은 프로세스 마다 다릅니다.
docs.microsoft.com
위 MSDN에 IOCP가 있는 것을 볼 수 있다.
IOCP 시스템을 관리하는 객체를 커널이 만들어서 우리에게 HANDLE를 넘겨주는 것이다.
IOCP 자료구조 | 설명 |
Device list | hDevice, CompletionKey로 구성되어 키값으로 값을 얻을 수 있게됨 |
IO Completion Queue | FIFO로 앞에서 부터 자료를 빼서 처리, I/O가 완료 되면 정보(이벤트)를 저장함 |
Waiting Thread Queue | LIFO로 마지막에 사용된 스레드를 다시 사용함, 스레드 ID가 저장됨 스레드 풀 역활 |
Released Thread List | Waiting Thread Queue(스레드 풀)에서 꺼내온 쓰레드 정보 |
Paused Thread List | Release쓰레드가 Suspend상태되면 저장되고, Suspend상태가 해제되면 다시 Released Thread List로 올라감 |
IOCP 특징 중 하나가, 쓰레드 풀로 쓰레드를 재사용하는 것인데,
IOCP 본인의 스레드 큐를 가지고 작업을 하고 완료된 IO가 있다면 알려준다.
재사용이 가능한 스레드를 유지하는 스레드 풀링은
WSASend/ WSARecv같은 비동기 입출력 함수를 쓰면
스레드 내부적으로 APC 큐가 생성이된다. 여기에 완료된 결과를 저장하는데
APC큐는 자동으로 생성되고 파괴된다. 그리고 그 큐는 본인 스레드에서만
확인이 가능하는데, 관리자 역활을 하는 IOCP는 모든걸 확인이 가능하다.
예를들어 관리자(IOCP)가 일꾼(1번 입출력), 일꾼(2번 입출력) 중
1번 일꾼의 IO작업이 끝났다하면 이를 처리하기 위해 Waiting Thread Queue에
가장 최근에 들어온 스레드를 깨운다. I/O가 완료될때마다 기다리고 쓰레드가 알아서 I/O작업을 해
성능이 좋다. -> 스레드 풀
또한, 특정 스레드에서 작업을 하던간에
IOCP 큐자체가 기타 스레드 결과가 저장이 된다.
즉, A 스레드에서 작업해도 B스레드에서도 확인이 가능하다.
1. IOCP 객체 핸들 생성
1 2 3 4 5 6 | HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads ); | cs |
CreateIoCompletionPort 함수는 세 가지의 역활을 한다.
- IOCP 포트만 생성
- 기존 IOCP 포트와 파일,소켓과 연결
- 생성 및 연결 모두 수행
처음 IOCP 객체 핸들 생성할때는 이런 식으로 생성한다.
1 2 | g_hIOCP = CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0, 0, 0); | cs |
마지막 인자 NumberOfConcurrentThreads는 0이면 시스템 프로세서에 있는 만큼
스레드를 허용하는 것이다. CPU 갯수보다 일부로 낮게 넣지 않는 이상, 0을 넣으면 된다.
2. IOCP와 소켓(파일) 연결
위의 CreateIoCompletionPort()는 세가지 역활을 할 수 있다고 위에 있다.
1 | CreateIoCompletionPort((HANDLE)clientSock, m_hIOCP, (ULONG_PTR)user, 0); | cs |
첫번째 인자 FileHandle에 소켓을 넣고 기존에 생성한 IOCP 핸들을 넣는다.
그리고 세번째 인자는 키값으로 입출력 완료가 되었을때 어떤 소켓이 완료
되었는지 알 수 있다.
3. WorkerThread 생성
IOCP는 스레드가 사실 없어도 동작을 한다. 하지만 IOCP가 여러 스레드를
동시에 컨트롤 한다는 이점으로 성능을 위해서는 필수이다.
쓰레드 갯수는 MSDN에서 CPU*2+1이란 숫자를 추천하는데,
이는 스레드가 Suspend되어 대기상태에 빠졌을때, 새로운 스레드를 꺼내기
위함이다라고 이해했다.
Iocp 기본 구조 이해 (slideshare.net)
Iocp 기본 구조 이해
IOCP IO Completion Port NHN NEXT 남현욱
www.slideshare.net
4. GetQueuedCompletionStatus()
완료큐에 완료된 입출력이 있다면 WorkerThread에서 작업을 하게 할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | DWORD WINAPI WorkerThread(LPVOID param) { KLobbyServer* pServer = (KLobbyServer*)param; DWORD dwTransfer; ULONG_PTR KeyValue; OVERLAPPED* pOverlapped; while (1) { //지정된 킬이벤트가 신호를 받았다면 break if (WaitForSingleObject(pServer->m_hKillEvent, 1) == WAIT_OBJECT_0) { break; } //완료큐에 데이터가 있으면 작업시작 BOOL bRet = ::GetQueuedCompletionStatus( pServer->m_hIOCP, &dwTransfer, &KeyValue, &pOverlapped, 1); KNetworkUser* pUser = (KNetworkUser*)KeyValue; KOV* pOV = (KOV*)pOverlapped; if (bRet == TRUE && pUser && pOV) { //작업 if (pOV->type == 1000 && dwTransfer == 0) { if(pUser->m_bConnect == true) pUser->m_bConnect = false; } else if(pOV->type == 1000) { pUser->Dispatch(dwTransfer, pOV); } } else { //오류 if (GetLastError() != WAIT_TIMEOUT) { ::SetEvent(pServer->m_hKillEvent); break; } } } return TRUE; } | cs |
//로비서버 IOCP 적용 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | #include "KLobbyServer.h" DWORD WINAPI WorkerThread(LPVOID param) { KLobbyServer* pServer = (KLobbyServer*)param; DWORD dwTransfer; ULONG_PTR KeyValue; OVERLAPPED* pOverlapped; while (1) { //지정된 킬이벤트가 신호를 받았다면 break if (WaitForSingleObject(pServer->m_hKillEvent, 1) == WAIT_OBJECT_0) { break; } //완료큐에 데이터가 있으면 작업시작 BOOL bRet = ::GetQueuedCompletionStatus( pServer->m_hIOCP, &dwTransfer, &KeyValue, &pOverlapped, 1); KNetworkUser* pUser = (KNetworkUser*)KeyValue; KOV* pOV = (KOV*)pOverlapped; if (bRet == TRUE && pUser && pOV) { //작업 if (pOV->type == 1000 && dwTransfer == 0) { if(pUser->m_bConnect == true) pUser->m_bConnect = false; } else if(pOV->type == 1000) { pUser->Dispatch(dwTransfer, pOV); } } else { //오류 if (GetLastError() != WAIT_TIMEOUT) { ::SetEvent(pServer->m_hKillEvent); break; } } } return TRUE; } bool KLobbyServer::Init(int port) { KServer::Init(port); m_hIOCP = CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0, 0, 0); SYSTEM_INFO system_info; GetSystemInfo(&system_info); //쓰레드 여러개 생성 for (int i = 0; i < system_info.dwNumberOfProcessors *2; i++) { DWORD id; //자기 자신의 서버 인자를 넘김. this m_hWorkThread[i] = CreateThread(0, 0, WorkerThread, this, 0, &id); } return true; } //게임서버는 new delete를 막아 //동적할당을 미리 메모리 메니져로 만들어서 해야함 bool KLobbyServer::AddUser(SOCKET clientSock, SOCKADDR_IN clientAddr) { KNetworkUser* user = new KNetworkUser(); user->Set(clientSock, clientAddr); u_long on = 1; ioctlsocket(clientSock, FIONBIO, &on); //유저리스트 추가 EnterCriticalSection(&m_cs); m_UserList.push_back(user); LeaveCriticalSection(&m_cs); //비동기 작업을 해야하니까 유저가 접속이되면 리시브를 걸어놔라 //유저에 대한 포인터를 넘긴다. ::CreateIoCompletionPort((HANDLE)clientSock, m_hIOCP, (ULONG_PTR)user, 0); //WSARecv를 건다. user->Recv(); //delete user; return true; } bool KLobbyServer::Run() { while (1) { //임계구역 EnterCriticalSection(&m_cs); //패킷 타입을 판별해서 해당 맞는 작업을 하면 됨 std::list<KPacket>::iterator iter_packet; for (iter_packet = m_lPacketPool.begin(); iter_packet != m_lPacketPool.end();) { switch ((*iter_packet).m_uPacket.ph.type) { case PACKET_USER_POSITION: { }break; case PACKET_CHAT_MSG: { }break; } } //주기적인 동기화 for (KNetworkUser* user : m_UserList) { if (user->m_lPacketPool.size() > 0) { Broadcast(user); } } //커넥트가 false면 나가는 처리까지 std::list<KNetworkUser*>::iterator user_iter; for (user_iter = m_UserList.begin(); user_iter != m_UserList.end();) { if ((*user_iter)->m_bConnect == false) { (*user_iter)->Disconnect(); delete (*user_iter); user_iter = m_UserList.erase(user_iter); std::cout <<"\nCurrent : " << m_UserList.size() <<" 명 접속중.."<<std::endl; } else { user_iter++; } } LeaveCriticalSection(&m_cs); } return true; } bool KLobbyServer::Release() { CloseHandle(m_hIOCP); KServer::Release(); return true; } | cs |
'STUDY > Network Programming' 카테고리의 다른 글
Network Programming - 소켓 입출력 모델 정리 종류 및 내용 (0) | 2022.01.26 |
---|---|
Network Programming - 비동기 파일 입출력이 가능한 WinAPI 파일 입출력 (Overlapped 입출력) (1) | 2022.01.14 |
Network Programming - 쓰레드 동기화 기법의 분류, 유저모드 크리티컬 섹션(CRITICAL_SECTION), 커널모드 Mutex(Mutual Exclusion) Semaphore 등 (0) | 2022.01.11 |
Network Programming - 윈도우 메세지 통해 소켓 입출력, WSAAsyncSelect 모델 (11) | 2022.01.10 |
Network Programming - 다중 입출력 함수 Select() (0) | 2022.01.07 |