01
19

 

이전 포스팅에서 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, 000);
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, 000);
 
    SYSTEM_INFO system_info;
 
    GetSystemInfo(&system_info);
 
    //쓰레드 여러개 생성
    for (int i = 0; i < system_info.dwNumberOfProcessors *2; i++)
    {
        DWORD id;
        //자기 자신의 서버 인자를 넘김. this 
        m_hWorkThread[i] = CreateThread(00, WorkerThread, this0&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

 

COMMENT