C++ IOCP 서버 3. IOCP 구현 (초기화)
* 아직 배우고 있는 학생이라 틀린 내용이 있을 수도 있습니다. 틀린 내용이 있다면 알려주시면 감사하겠습니다.
IOCP 이론 : https://developstudy.tistory.com/43?category=836040
이번 포스팅에서는 IOCP 사용법을 포스팅할 것이다.
IOCP 소켓을 구현하기 전에 먼저 해야 할 것은 오버랩 구조체를 만드는 것이다.
1. overlapped 구성
나는 오버랩을 아래와 같이 구성했다.
IOCP에서 사용되는 각종 비동기 명령들에 태워 보낼 Overlapped들이다.
FOverlapped에서 CPlayer는 접속한 유저의 정보를 담는 클래스이고,
FAcceptOverlapped에서 _accept는 접속한 유저의 엔드포인트를 담는 구조체이다.
이제 이 Overlapped들을 저번에 구현한 ObjectPool을 가지고 사용할 것이다!
(https://developstudy.tistory.com/44?category=836040)
이제 맨~~~처음으로 돌아가서 IOCP 서버를 초기화해보자.
2. IOCP 서버 초기화~리슨
// Init WSA
WSAData wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
먼저 WSA 함수를 사용하기 위해 WSA를 초기화시켜준다.
// Create IOCP
hcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (hcp == nullptr) {
WriteLog(ELogLevel::Error, "Failed to Create IOCP");
return false;
}
return true;
다음은 IOCP를 초기화시켜준다.
CreateIoCompletionPort의 맨 처음 인자에 INVALID_HANDLE_VALUE를 넣으면 초기화된다고 한다.
여기서 hcp가 초기화되는 걸 확인하자.
hcp는 IOCP에서 사용되는 핸들이다.
// 소켓 초기화 부분
const int SOCET_OPTION_CONDITIONAL_ACCEPT = 0x3002;
_socket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (_socket == -1) {
WriteLog(ELogLevel::Error, "Failed to create socket.");
return;
}
bool on = true;
if (setsockopt(_socket, SOL_SOCKET, SOCET_OPTION_CONDITIONAL_ACCEPT, (char *)&on, sizeof(on)))
{
WriteLog(ELogLevel::Error, "Can't set SO_CONDITIONAL_ACCEPT.");
return;
}
// 위에서 만든 소켓을 오브젝트 풀로 꺼낸다.
_tcpListenSocket = _socketObjects->PopObject();
맨 처음 오브젝트 풀에서 Socket을 초기화시켜준다.
소켓은 WSASocket 함수로 생성하여 Internet, STREAM(TCP), WSA_FLAG_OVERLAPPED 옵션을 주고,
SO_CONDITIONAL_ACCEPT(https://docs.microsoft.com/en-us/windows/desktop/winsock/so-conditional-accept)
옵션을 등록해준다.
그다음 리슨 소켓으로 사용하기 위해 오브젝트 풀에서 꺼낸다.
// Bind socket to IOCP
CreateIoCompletionPort((HANDLE)_tcpListenSocket->GetSocket(), hcp, _tcpListenSocket->GetSocket(), 0);
// Bind
FSocketAddrIn serveraddr;
ZeroMemory(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(TCP_SERVER_PORT);
if (bind(_tcpListenSocket->GetSocket(), (FSocketAddr*)&serveraddr, sizeof(serveraddr)) == SOCKET_ERROR) {
WriteLog(ELogLevel::Error, "Failed to bind.");
return false;
}
int enable = 0;
if (setsockopt(_tcpListenSocket->GetSocket(), SOL_SOCKET, SO_CONDITIONAL_ACCEPT, (const char*)&enable, sizeof(int)) == SOCKET_ERROR)
{
WriteLog(ELogLevel::Warning, CLog::Format("setsockopt so_conditional_accept Error : ", WSAGetLastError()));
return false;
}
int rcvBufSize = PACKET_BUFSIZE;
if (setsockopt(_tcpListenSocket->GetSocket(), SOL_SOCKET, SO_RCVBUF, (const char*)&rcvBufSize, sizeof(int)) == SOCKET_ERROR)
{
WriteLog(ELogLevel::Warning, CLog::Format("setsockopt SO_RCVBUF Error : ", WSAGetLastError()));
return false;
}
// listen
if (listen(_tcpListenSocket->GetSocket(), SOMAXCONN) == SOCKET_ERROR) {
WriteLog(ELogLevel::Error, "Failed to listen.");
return false;
}
다음 현재 소켓을 IOCP에 묶어준다.
아까 사용했던 CreateIoCompletionPort에 첫 번째와 세 번째 인자에 소켓을 넣고 두 번째 인자에 아까 얻어낸 hcp를 넣어서 묶을 수 있다.
그 이후 Bind->SO_CONDITIONAL_ACCEPT OFF->SO_RCVBUF을 설정해주고 리슨을 시작해준다.
이제 리슨 준비는 끝났다!
리슨을 받는 부분은 조금 미루고 작업자 쓰레드(Worker Thread)를 먼저 만들자.
3. 작업자 쓰레드(Worker Thread) 만들기!
DWORD WINAPI CIOCPServer::WorkerThread(LPVOID arg)
{
int retval;
CIOCPServer* owner = (CIOCPServer*)arg;
HANDLE& hcp = owner->hcp;
DWORD cbTransferred;
SOCKET client_sock;
FOverlapped* overlap;
while (true) {
// Wait until Async IO end
retval = GetQueuedCompletionStatus(hcp, &cbTransferred, &client_sock,
(LPOVERLAPPED *)&overlap, INFINITE);
try {
switch (overlap->m_type)
{
case EOverlappedType::ACCEPT:
{
owner->_AcceptProc((FAcceptOverlapped*)overlap);
break;
}
case EOverlappedType::TCP_RECV:
{
owner->_RecvProc((FBuffuerableOverlapped*)overlap, cbTransferred, true);
break;
}
case EOverlappedType::UDP_RECV:
{
owner->_RecvProc((FBuffuerableOverlapped*)overlap, cbTransferred, false);
break;
}
case EOverlappedType::SEND:
{
owner->_SendProc((FSendOverlapped*)overlap);
break;
}
case EOverlappedType::CLOSE:
{
owner->_CloseProc((FCloseOverlapped*)overlap);
break;
}
}
}
catch (...) {
owner->WriteLog(ELogLevel::Error, CLog::Format("[ReceiveData Exception] %s", inet_ntoa(overlap->m_player->addr.sin_addr)));
owner->ServerNetworkSystem->CloseConnection(overlap->m_player);
delete overlap;
}
}
return 0;
}
GetQueuedCompletionStatus 함수를 사용하면 완료된 IOCP 입출력이 나올 때까지 기다리다가 완료되면 흐름이 진행된다.
이 함수를 통해 튀어나온 overlapped으로 무슨 작업이 완료됐는지 판단해서 각 Proc로 넘겨주었다.
각 Proc은 다음 포스팅에서 완성할 것이다.
try catch는 혹시 몰라서 넣은 것..
그리고 이 작업자 쓰레드들을 실행해주자!
// Check CPU
SYSTEM_INFO si;
GetSystemInfo(&si);
// Create Worker Thread
HANDLE hThread;
for (int i = 0; i < (int)si.dwNumberOfProcessors * 2; ++i) {
hThread = CreateThread(NULL, 0, WorkerThread, (LPVOID*)this, 0, nullptr);
if (hThread == nullptr) {
WriteLog(ELogLevel::Error, "Failed to Create Thread");
return false;
}
CloseHandle(hThread);
}
현재 컴퓨터 CPU 코어수의 2배만큼의 작업자 쓰레드를 만들어준다.
인자로는 this를 넘겨준다.
이제 2.가 끝난 시점에 이 함수를 돌려 작업자를 실행시키면 된다.
초기화가 거의 다 끝났지만 아직 한 가지 덜 한 것이 있다. 그것은 바로 UDP 소켓!
4. UDP Recv Socket 만들기
_udpRecvSocket = WSASocket(PF_INET, SOCK_DGRAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ZeroMemory(&_udpServeraddr, sizeof(FSocketAddrIn));
_udpServeraddr.sin_family = AF_INET;
_udpServeraddr.sin_addr.s_addr = htonl(INADDR_ANY);
_udpServeraddr.sin_port = htons(UDP_SERVER_PORT);
if (bind(_udpRecvSocket, (FSocketAddr*)&_udpServeraddr, sizeof(_udpServeraddr)) == SOCKET_ERROR) {
WriteLog(ELogLevel::Error, "Failed to bind.");
return false;
}
int rcvBufSize = PACKET_BUFSIZE;
if (setsockopt(_udpRecvSocket->GetSocket(), SOL_SOCKET, SO_RCVBUF, (const char*)&rcvBufSize, sizeof(int)) == SOCKET_ERROR)
{
WriteLog(ELogLevel::Warning, CLog::Format("setsockopt SO_RCVBUF Error : ", WSAGetLastError()));
return false;
}
CreateIoCompletionPort((HANDLE)_udpRecvSocket, hcp, _udpRecvSocket, 0);
UDP는 TCP와 거의 동일하다.
차이점이 있다면 Listen을 하지 않기 때문에 SO_CONDITIONAL_ACCEPT 설정이 없다는 것?
이제 초기화는 진짜 끝~~~
다음 포스팅에서는 접속, 접속 종료, 송신, 수신 처리를 구현하는 부분을 포스팅해야겠다.