IOCP를 사용한 간단한 채팅
프로그램 서버 만들기
김
준 한
전체적인 구성도
위에서 보는 것처럼 IOCP의 기본적은 구성은 두개의 쓰레드로 만들어 진다. 왼편에
있는 루프는 보통 main() 함수에서 있는 루프고 오른쪽에 있는 루프는 main함수가 루프를 들어가기 전에 생성해야 하는 쓰레드로 보통
Worker쓰레드라고 한다. 권장하는 Worker쓰레드의 수는 하나의 CPU당 하나로 되어 있다. 또한 2번에서 3번으로의 데이터 전송은 전역변수를
이용하거나 Worker쓰레드를 생성할 때, 포인터를 전송하여 두 루프가 변수를 공유하는 방법이다. 이에 대해서는 달리 자세히 설명하지는 않겠다.
1.
Wait for client to connect
이 과정에 이전에 이미 IOCP 핸들과 소켓 초기화, 클라이언트의 접속을 대기하는
소켓, 접속한 클라이언트들의 정보를 가지는 배열( 물론, 자료구조는 자신의 선택이다. )을 생성해 두어야 한다.
여기는 클라이언트의 접속을 대기하는 소켓에 Accept()를 호출하여 클라이언트가
접속하기를 기다리는 과정이다.
2.
Open communication channel for client
1번 과정에서 클라이언트가 접속을 하면 여기서는 올바른 소켓인지 판단하고, 접속한
소켓을 클라이언트 정보를 저장하는 배열에 저장하고, IOCP에 해당 소켓을 등록하고, 소켓에 읽기 작업을 신청한다. ( 읽기를 먼저 신청하는 이유는
대해서는 설명하지 않겠다. ) 이 때 중요한 것은 현재 WSARecv(), ReadFile()함수를 사용하여 읽기 작업을 신청했지만, IOCP는
작업의 완료 시에 읽기 작업을 끝냈는지 쓰기 작업을 끝냈는지 가르쳐 주지 않는다는 것이다. 따라서, OVERLAPPED구조체를 상속( C관점에서는
새로운 구조체를 선언할 때, OVERLAPPED구조체를 제일 첨에 위치하는 멤버로 선언하여 사용할 수 있다.)하여 읽기 작업을 하는 건지 쓰기
작업을 하는 건지에 대한 흔적을 남겨야 한다.
3.
Read Request from client
이 과정에서 GetQueuedCompletionStatus() 함수를 사용하여
현재 등록된 소켓들 중에 읽기나 쓰기 작업이 완료된 것이 있는지 확인한다. 이 때, IOCP가 알려주는 정보는 2번 과정에서 등록 할 때, 소켓과
같이 입력했던 KEY와 I/O작업을 신청할 때, 인자로 넘겨주었던 OVERLLAPPED 구조체( 확장을 시켰다면 확장 구조체형으로 캐스팅 해주면
된다.)의 주소이다.
4.
Excute request locally
이제 3번 과정에서 받은 key와 OVERLAPPED구조체를 가지고 I/O작업을
신청했을 때, 만약 작업이 끝나면 했었을 작업을 해주면 된다.
예를 들어, 일반 블록되는 I/O작업을 하는 상황에서라면,
//Recv할 때,
char *szBuffer;
szBuffer = new char[512];
Recv( szBuffer, … );
// szBuffer의 내용에 대한 처리를 한다.
Printf( “%s\\n”, szBuffer );
Delete szBuffer;
//Send할 때,
char *szBuffer;
szBuffer = new char[512];
sprintf( szBuffer, “전송할 내용을 입력한다.” );
Send( szBuffer, … );
Delete szBuffer;
위와 같은 식으로 프로그램을 작성할 것이다. 메모리를 동적으로 할당한 이유는
I/O작업이 끝나기 전까지 데이터가 있는 메모리는 유효해야 하기 때문이다. 이 프로그램의 단점은 I/O작업이 끝나지 않으면 CPU는 놀고 있게
된다는 것이다. 그걸 해결하기 위해서 나타난 것이 IOCP이다.
IOCP에서는 Recv()/Send() 함수 이전 부분은 2번 과정 또는, 다른
곳에서 하고, Recv()/Send()함수 다음 부분은 4번 과정에서 하겠다는 것이다. 그러므로, I/O작업을 신청할 당시의 데이터( KEY,
OVERLAPPED구조체)를 저장해야 하는 것이다.
이 과정에서는 현재 완료된 작업이 읽기 작업인지 쓰기 작업인지를 구분하여 처리해 주어야 한다.
If( pOverlapped->mode == 읽기 )
{
//읽기
작업을 완료했을 때 할 일을 한다.
//보통
여기서 다시 읽기 작업을 신청하게 된다. ( 이유는 설명하지 않겠다. )
}
else if( pOverlapped->mode == 쓰기 )
{
//쓰기
작업을 완료했을 때 할 일을 한다.
}
else
{
//에러가
발생했다.
}
5.
Return result to client
이 과정은 사실 4번 과정과 같이 포함되는 부분이다. 즉, 채팅을 예로 들면,
어떤 사용자가 메시지를 입력했을 때, 4번과정의 If( pOverlapped->mode == 읽기 ) {} 블록안이 실행될 것이고, 어떤
메시지인지 확인한 후, 접속한 모든 사용자에게 메시지를 보내게 된다.
필요한 자료들
l
IOCP 핸들
반드시 필요한 것이다.
l
확장 OVERLAPPED구조체 또는 클래스
#define READ 1
#define WRITE 2
class CXOverlapped : public OVERLAPPED
{
public:
int
mode;
};
struct XOVERLAPPED
{
OVERLAPPED
over;
Int
mode;
Int
iNum;
Char
szRecv[512];
}
위에서 CXOverlapped를 사용하도록 하겠다.
l
클라이언트 정보를 저장할 공간
SOCKET g_sock[MAX_CLIENT];
구현한 소스
// IOCP.cpp
#define
WIN32_LEAN_AND_MEAN //
Exclude rarely-used stuff from Windows headers
#include
#include
#include
#define
MY_RECV_BUFFER 512
#define
MY_CHAT_PORT 50000
#define
MY_MAX_CLIENT 10
#define
MY_MAX_PACKET 100
#define READ 1
#define WRITE 2
class CXOverlapped
: public OVERLAPPED
{
public:
int
mode; //
Read냐 Write냐..결정.
char
szRecv[MY_RECV_BUFFER];
int
iNum; //
Send할 때, 같은 데이터를 여러 개 만들지
// 않기 위해서 사용하는 변수이다.
// 원리는 이 구조체를 사용하여 Send를 하면
// iNum값을 1씩 증가 시킨다. 작업 완료 처리
// 루틴에서 Send가 완료 되었으면 이 값을 1씩
// 빼주고 0이 된 구조체를 delete한다.
CXOverlapped()
{
Clear();
mode
= READ;
}
~CXOverlapped()
{
}
inline
void Clear()
{
Internal
= 0;
InternalHigh
= 0;
Offset
= 0;
OffsetHigh
= 0;
hEvent
= 0;
}
};
SOCKET
g_sockListen; //클라이언트의 접속을
대기하는 소켓
int g_nClients = 0; //현재
몇명의 클라이언트가 접속했는지 저장.
SOCKET g_asockClients[MY_MAX_CLIENT];
HANDLE
g_hCompletionPort;
// 소켓 초기화 함수.
BOOL InitSocket()
{
WORD
wVer;
WSADATA
wsaData;
SOCKADDR_IN
serv_addr;
wVer
= MAKEWORD(1,1);
if(WSAStartup(wVer,
&wsaData) != 0)
{
printf(
"WSAStartup() 실패 : %d\\n", WSAGetLastError());
return
FALSE;
}
g_sockListen
= socket(AF_INET, SOCK_STREAM, 0);
if
( g_sockListen == INVALID_SOCKET )
{
printf(
"socket() 실패 : %d\\n", WSAGetLastError());
return
FALSE;
}
ZeroMemory
(&serv_addr, sizeof (serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_port
= htons(MY_CHAT_PORT);
if(
bind( g_sockListen, (LPSOCKADDR)&serv_addr, sizeof(serv_addr) ) ==
SOCKET_ERROR )
{
printf(
"bind() 실패 : %d\\n", WSAGetLastError());
return
FALSE;
}
if
(listen(g_sockListen, 5) == SOCKET_ERROR)
{
printf(
"listen() 실패 : %d\\n", WSAGetLastError());
return
FALSE;
}
printf("g_sockListen
소켓 초기화 성공\\n");
return
TRUE;
}
void SendAll(
CXOverlapped* pPacket )
{
CXOverlapped
*pSendPacket = new CXOverlapped;
strcpy(
pSendPacket->szRecv, pPacket->szRecv );
pSendPacket->mode
= WRITE;
pSendPacket->iNum
= 0;
for(
int i = 0; i < g_nClients; i++ )</span>
{
int
len = strlen( pSendPacket->szRecv );
if(
INVALID_SOCKET == g_asockClients[i] )
continue;
if(
FALSE == WriteFile( (HANDLE)g_asockClients[i],
pSendPacket->szRecv,
len,
NULL,
pSendPacket
) )
{
if(
GetLastError() != ERROR_IO_PENDING )
{
printf("WSARecv()
failed with error %d\\n", WSAGetLastError());
return;
}
}
pSendPacket->iNum++;
}
}
DWORD WINAPI
WorkerThread( void* pModel )
{
DWORD
dwBytesTransferred;
DWORD
dwCompKey;
CXOverlapped*
pPacket;
LPOVERLAPPED
pOverlap;
printf(
"Worker 시작\\n" );
while(
1 )
{
//////////////////////////////////////////
//3.
Read Request from client
//////////////////////////////////////////
if(
FALSE == GetQueuedCompletionStatus( g_hCompletionPort, &dwBytesTransferred,
&dwCompKey,
(LPOVERLAPPED *)&pOverlap, INFINITE ) )
{
if(
pOverlap != NULL )
{
printf(
"Error Thread : GetQueueCompletionStatus( %d )\\n",
GetLastError()
);
return
0;
}
}
if(
dwBytesTransferred == 0 )
{
printf("Closing
socket %d\\n", g_asockClients[dwCompKey] );
if(
SOCKET_ERROR == closesocket( g_asockClients[dwCompKey] ) )
{
if(GetLastError()
== 10038)
{
continue;
}
else
{
printf("closesocket()
failed with error %d\\n", WSAGetLastError());
return
0;
}
}
continue;
}
//////////////////////////////////////////
//4.
Excute request locally
//////////////////////////////////////////
pPacket
= (CXOverlapped*)pOverlap;
if(
READ == pPacket->mode )
{
printf(
"Recv: %s\\n", pPacket->szRecv );
pPacket->Clear();
if(
FALSE == ReadFile( (HANDLE)g_asockClients[dwCompKey],
pPacket->szRecv,
MY_RECV_BUFFER, NULL, pPacket ) )
{
if(
GetLastError() != ERROR_IO_PENDING )
{
printf("WSARecv()
failed with error %d\\n", WSAGetLastError());
return
0;
}
}
if(
pPacket->szRecv[0] == NULL )
continue;
//////////////////////////////////////////
//5.
Return result to client
//////////////////////////////////////////
SendAll(
pPacket );
}
else
if( WRITE == pPacket->mode )
{
printf(
"Send complete to %d packet(%d)\\n", dwCompKey, pPacket->iNum
);
if(
--(pPacket->iNum) <= 0 )</span>
delete
pPacket;
}
else
printf(
"Error\\n" );
}//while(1)
}
int main()
{
//
모든 과정을 진행 하기 전에 해야 하는 일들.
InitSocket();
g_hCompletionPort
= CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );
if(
INVALID_HANDLE_VALUE == g_hCompletionPort )
{
printf(
"IOCP create error..\\n" );
return
0;
}
for(
int i = 0; i < MY_MAX_CLIENT; i++ )</span>
g_asockClients[i]
= 0;
//
쓰레드 생성.
DWORD
ThreadId;
HANDLE
ThreadHandle;
ThreadHandle
= CreateThread( NULL, 0, WorkerThread,
NULL, 0, &ThreadId );
if
(!ThreadHandle)
{
fprintf
(stdout, "Create Worker Thread Failed\\n");
return
FALSE;
}
CloseHandle
(ThreadHandle);
while(
1 )
{
//////////////////////////////////////////
//1.
Wait for client to connect