Programming - cpueblo.com

[강좌] IOCP를 사용한 머그게임 서버 만들기


글쓴이 : 유광희 날짜 : 2002-12-17 (화) 19:34 조회 : 13520
_ xml_ns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"> IOCP를 사용한 머그게임 서버만들기

IOCP를 사용한 머그게임 서버만들기

1. 프로토콜이란?

프로토콜이란 한 컴퓨터와 다른 컴퓨터가 통신하는 방식을 정의한 것이다. 즉, 원거리에 있는 두 사람이 서로 교신을 할 때, 빨간 깃발을 들으면 누군가 공격해온다는 의미이고, 파란색 깃발을 들면 비가 온다는 의미라고 정하는 것 이것이 프로토콜이다.

일반적으로 머그게임 또는 대부분의 인터넷을 경유하는 통신에서 프로토콜을 만들 때 TCP/IP 기반에서 프로토콜을 작성한다고 한다. 이는 TCP/IP의 프로토콜을 사용하여 자신만의 프로토콜을 만드는 것을 의미한다. 어쨌든 프로토콜에 대한 자세한 내용은 다른 서적을 참조하기 바란다. 여기서, 중요한 것은 네트워크를 통해서 컴퓨터끼리 통신을 하기 위해서는 프로토콜을 정의 해야 한다는 것만 알면 충분하다.

어떤 의미를 가진 것을 이제부터 패킷이라고 부르자. 즉 하나의 프로토콜을 여러 개의 패킷으로 이루어졌다. 누군가 공격을 해 올 때는 빨간 깃발을 들어라. 여기서 빨간 깃발이 패킷이 되는 것이다.

이제 머그게임에서 나올만한 상황 중에 하나인 이동에 대한 패킷을 만들어보자. 이동 패킷이 어떤 정보를 가지고 있을 것인가는 프로그래머의 마음이다. 필자는 이 패킷이 어떤 패킷인지에 대한 정보( Command )와 이동위치(X, Y)의 정보를 가지는 패킷을 만들도록 하겠다.

BYTE[4] Command

BYTE[4] X 좌표

BYTE[4] Y 좌표

위와 같이 이동 패킷이 만들어졌다. 각각의 정보가 4바이트로 이루어진 12바이트의 패킷이 만들어졌다. 이와 같이 머그게임의 서버와 클라이언트가 서로 전달해야 할 모든 메시지에 대해 패킷을 만드는 것이 프로토콜 작성이다.

한가지 더 알아야 하는 것은 네트워크에서는 실제로 자신이 원하는 만큼 쓰고 원하는 만큼 읽을 수 없다는 것이다.

send( socket, packet, size, 0 );

recv( socket, packet, size, 0 );

다음과 같은 두 코드를 보고 프로그래머는 항상 packet버퍼에 있는 내용을 size만큼 쓰고 읽을 수 있다고 생각하면 안 된다. 이는 네트워크 상에서 발생하는 여러 가지 문제에 의해서 얼마를 읽고 쓸 수 있는지 결정이 된다.( 자세한 내용은 관련서적 참조 ) 따라서, 프로그래머는 항상 원하는 만큼의 데이터를 읽고 썼는지에 대해 확인을 해야 한다. 어떻게 원하는 만큼의 데이터를 읽고 쓸 수 있는지는 뒤에서 설명하겠다.

위와 같은 이유 때문에 우리가 만드는 패킷에는 길이에 대한 정보가 추가 되어야 한다. 즉, 위에서 만든 이동 패킷은 다음과 같이 변경되어야 한다.

BYTE[2] 패킷의 길이

BYTE[4] Command

BYTE[4] X 좌표

BYTE[4] Y 좌표

2바이트의 패킷 길이가 추가 되었다. 필자는 패킷이 아무리 길어도 65535바이트를 넘지 않을 것이라고 생각해서 패킷의 길이를 2바이트로 나타내었다. 그러나 자신은 4바이트 또는 1바이트로 만들겠다고 한다면 그렇게 해도 상관없다.

이제 다음에서는 위와 같이 만든 패킷들을 어떻게 쓰고 읽는 지에 대해서 알아보도록 하겠다.

2. 패킷 쓰기와 읽기

먼저, 패킷을 쓰는 것에 대해서 알아보도록 하자. 사실 쓰는 것에 대해서는 별로 걱정할 것이 없다.

send( socket, packet, size, 0 );

위와 같이 우리가 아는 방식대로 쓰기만 하면 된다. 단 IOCP사용하기IOCP로 채팅만들기에서 말했던 것과 같이 중첩입출력의 경우 packet의 내용은 작업이 끝나기 전에는 변경되면 안 된다는 사실만 기억하기 바란다.

패킷을 읽는 것은 쓰는 것과는 다르게 약간의 처리를 해주어야 하는데 이는 1장에서 패킷에 패킷 길이를 추가한 이유이기도 하다.

int nRead;

while ((nRead = recv(hSocket, g_szPacket, PACKET_BUFFER - g_nPI, 0)) > 0)

{

g_nRI += nRead;

while (g_nPI < <span class=SpellE>g_nRI)

{

if (g_mode == PACKET_LENGTH)

{

if (g_nPI + 2 <= <span class=SpellE>g_nRI)

{

g_sPacketLen = g_szPacket[0] + (g_szPacket[1] << 8) - 2;</span>

g_nPI += 2;

g_mode = PACKET_BODY;

}

else

break;

}

else if (g_mode == PACKET_BODY)

{

if (g_nPI + g_sPacketLen <= <span class=SpellE>g_nRI)

{

if (g_szPacket[g_nPI] >= S_MAX)

{

}

else

{

Packet(g_szPacket + g_nPI);

}

g_nPI += g_sPacketLen;

g_mode = PACKET_LENGTH;

}

}

}

if( g_nPI == g_nRI )

{

g_nPI = g_nRI = 0;

}

else if (g_nPI > 0)

{

memmove(g_szPacket, g_szPacket + g_nPI, g_nRI - g_nPI);

g_nRI -= g_nPI;

}

1 프로그램세계 12월호 특집기사중 (TCP/IP기반의 멀티플레이어 온라인 게임제작 관련화일에서 발취) ( 이에 대한 설명은 프로그램세계 12월호에 있을거라고 생각합니다. 이해 안되시면 질문 주세요 )

위의 소스는 하나의 패킷을 정확히 읽을 수 있는 가장 좋은 방법이라고 생각하는 소스이다. 우리가 만들 머그 서버에서도 위와 같은 알고리즘으로 패킷을 읽어 오도록 하겠다.

3. 이 모든 것을 종합하여 머그게임 서버를 작성하자.

먼저 우리가 머그게임 서버를 만들기 위해서 필요한 것들을 생각해보도록 하자.

l ICOP 핸들

IOCP채팅서버만들기 참조

l 확장 OVERLAPPED구조체 또는 클래스

IOCP채팅서버만들기 참조

l 클라이언트 정보를 저장할 공간

ICOP채팅서버만들기에서는 클라이언트의 소켓만을 저장했지만, 패킷 읽기를 구현해야 하기 때문에 많은 정보가 필요하다.

Class CClient

{

public:

enum { PACKET_BODY, PACKET_LENGTH };

enum { PACKET_BUFFER = 512 };

SOCKET m_Socket;

int m_nPI;

int m_nRI;

short m_nPacketLen;

};

Ccleint g_ClientC[MAX_CLIENT];

지금은 서버를 구성하는 것이 중심이므로 클래스를 다음과 같이 만들지만, 위와 같이 GetXXX(), SetXXX()로 만들어지는 클래스는 프로그래머가 잘못 클래스를 작성했다고 생각하면 틀림없다.


4. 채팅서버 프로그램을 확장하자

// IOCP.cpp

#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers

#include <<span class=SpellE>windows.h>

#include

#include <<span class=SpellE>stdio.h>

#define MY_RECV_BUFFER 512

//#define MY_CHAT_PORT 50000

#define MY_MAX_CLIENT 10

#define MY_MAX_PACKET 100

class CClient

{

public:

enum MODE { PACKET_BODY, PACKET_LENGTH };

SOCKET m_Socket;

int m_nPI;

int m_nRI;

short m_nPacketLen;

MODE m_eMode;

public:

CClient()

{

m_Socket = INVALID_SOCKET;

m_nPI = m_nRI = 0;

m_nPacketLen = 0;

m_eMode = PACKET_BODY;

}

};

#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; //현재 몇명의 클라이언트가 접속했는지 저장.

CClient g_ClientsC[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 PacketProcess( char* pPacket )

{

// 여기서 패킷에 대한 처리를 하며 된다.

}

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_ClientsC[dwCompKey] );

if( SOCKET_ERROR == closesocket( g_ClientsC[dwCompKey].m_Socket ) )

{

if(GetLastError() == 10038