Programming - cpueblo.com

[강좌] IOCP 로 채팅 서버 만들기


글쓴이 : 유광희 날짜 : 2002-12-17 (화) 19:43 조회 : 16637


IOCP를 사용한 간단한 채팅 프로그램 서버 만들기

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

Downloads