처음에는 쓸까 말까 고민했지만
졸업작품에 사용하는 게임 서버 제작기를 간단하게 쓰기로 결정했습니다.
아직 배울 것이 한참 많아 틀린 부분이나 좋지 않은 부분이 많겠지만 양해 부탁드립니다.
혹시 틀린 점이나 고치면 좋은 부분이 있다면 언제든지 알려주세요!
서버 및 클라이언트 개발 환경
서버: C++, windows
클라이언트: C#, Unity
서버와 클라이언트 둘 다 기본적으로 비동기 방식을 이용했습니다.
첫 번째는 서버와 측의 기본 네트워크 코드에 대해서 작성해 보려합나다.
(이미 연동되고 게임 로직을 짜고 있긴 하지만... 처음부터 한다는 느낌으로...)
처음에는 저희 졸업작품 팀에서 클라이언트가 C#이라서 서버도 C#으로 만드는게 편할까?
라는 생각을 했습니다. 라이브러리 공유도 힘들고 C#, C++ 왔다갔다 하면서 비슷한 코드를 두 번 만들어야하고...
(물론 CLR나 P/Ivoke 방식도 고민했습니다.)
차근차근 진행해보겠습니다!
일단 서버 객체입니다.
크게 보면 IOCPManager, SessionManager, LogicManager 이렇게 세가지로 이루어져 있습니다.
IOCPManager는 기본적으로 IO작업의 처리를 담당합니다.
SessionManager는 SessionPool을 가지고 있고 Accept 처리와 각 세션에 대한 총괄 관리를 합니다.
LogicManager는 분류된 패킷들을 처리합니다.
먼저 IOCPManager 를 작성하겠습니다.
1. 윈도우 소켓을 초기화 한 후 IOCP를 만들어줍니다.
// Winsock Init
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return false;
// Create IOCP
m_CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
NULLCHEACK_B(m_CompletionPort, "Create CompletiontPort Error...");
SYSTEM_INFO si;
GetSystemInfo(&si);
m_iocpThreadCount = si.dwNumberOfProcessors * 2;
// Make Thread
HANDLE hThread;
for (int i = 0; i < m_iocpThreadCount; i++)
{
hThread = CreateThread(NULL, 0, IoCompletionThread, m_CompletionPort, 0, NULL);
NULLCHEACKB(hThread);
CloseHandle(hThread);
}
2. 그다음 소캣 모드에 따라 소켓을 만들어 줍니다.
switch (param.sockMode)
{
case SocketMode::TCP:
CreateTCPSocket(param.port);
break;
case SocketMode::UDP:
CreateUDPSocket();
break;
case SocketMode::DUAL:
CreateTCPSocket(param.port);
CreateUDPSocket();
break;
default:
ERRQUIT("[IOCP Initialize]Wrong Socket Mode");
return false;
}
소켓을 IOCP와 연결하고 소켓을 바인드하고.. 서버에서 리슨 준비를 해줍니다.
3. 다음은 worker-thread에서 사용할 함수를 만듭니다.
뭔가 길지만 차근차근 이야기해보겠습니다.
DWORD __stdcall LunaNet::IOCPManager::IoCompletionThread(LPVOID arg)
{
int retVal;
HANDLE hcp = (HANDLE)arg; // Completion Port Handle
while (true)
{
DWORD cbTransferred;
SOCKET client_sock;
ClientToken* token;
// IO
retVal = GetQueuedCompletionStatus(hcp, &cbTransferred,
(PULONG_PTR)&client_sock, (LPOVERLAPPED*)&token, INFINITE);
token->byteTransperred = cbTransferred;
if (retVal == 0 || cbTransferred == 0)...
NULLCHEACK_B(token, "Token Null");
switch (token->m_lastOperation)
{
case SocketOperation::SO_ZeroByteRecv: // page-locking 방지
token->m_session->Receive();
token->m_session->ReturnToken(token);
break;
case SocketOperation::SO_Accept:
token->m_session->AcceptComplete(token);
break;
case SocketOperation::SO_Connect:
break;
case SocketOperation::SO_Disconnect:
token->m_session->DisconnectComplete(token);
break;
case SocketOperation::SO_Send:
token->m_session->SendComplete(token);
break;
case SocketOperation::SO_Receive:
token->m_session->ReceiveComplete(token);
break;
case SocketOperation::SO_SendTo:
token->m_session->SendToComplete(token);
break;
case SocketOperation::SO_ReceiveFrom:
token->m_session->ReceiveFromComplete(token);
break;
default:
ERRQUIT("Invalid Operlation");
break;
}
}
return 0;
}
우선 ClientToken은 OVERLAPPED 구조체 입니다.
이 구조체에는 OVERLAPPED구조체, WSABUF, 해당 세션 정보, 바이트 길이 등으로 이루어져 있습니다.
GetQueuedCompletionStatus에 작업이 있으면 해당 IO작업 때 사용된 토큰을 가지고 옵니다.
그럼 해당하는 명령에따라 분기되어 동작을 실행해주게 됩니다.
Switch 문에 있는 enum 값들은 아래와 같은 구성으로 이루어져 있습니다.
enum class SocketOperation
{
SO_None = 0,
SO_Accept, // 서버일때 연결 수락시
SO_Connect, // 연결 성공시
SO_Disconnect, // 연결 해제시
SO_Send, // 전송 (TCP)
SO_SendTo, // 전송 (UDP)
SO_Receive, // 받기 (TCP)
SO_ReceiveFrom, // 받기 (UDP)
SO_ZeroByteRecv, // 페이지 락을 방지하기 위한 Zero-byte-Recv
};
작업에 따라 해당 세션별로 완료 처리를 해줍니다.
이번 글은 여기까지입니다. 감사합니다
다음은 서버에서 세션별로 어떤식으로 처리하고 구성했는지 적어보겠습니다.
댓글
댓글 쓰기