졸업 작품/서버

C++ IOCP 서버 3. IOCP 구현 (초기화)

장형이 2019. 5. 15. 02:43

* 아직 배우고 있는 학생이라 틀린 내용이 있을 수도 있습니다. 틀린 내용이 있다면 알려주시면 감사하겠습니다.

 

IOCP 이론 : https://developstudy.tistory.com/43?category=836040

 

C++ IOCP 서버 2. IOCP 이론

* 아직 배우고 있는 학생이라 틀린 내용이 있을 수도 있습니다. 틀린 내용이 있다면 알려주시면 감사하겠습니다. IOCP는 Window 환경에서 작동하는 제일 흔히 쓰이는 논블로킹 프로세스이다. 최소한의 쓰레드로 최..

developstudy.tistory.com

이번 포스팅에서는 IOCP 사용법을 포스팅할 것이다.

 

IOCP 소켓을 구현하기 전에 먼저 해야 할 것은 오버랩 구조체를 만드는 것이다.

 

1. overlapped 구성

 

나는 오버랩을 아래와 같이 구성했다.

 

Overlapped UML

 

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 설정이 없다는 것?

 

이제 초기화는 진짜 끝~~~

 

다음 포스팅에서는 접속, 접속 종료, 송신, 수신 처리를 구현하는 부분을 포스팅해야겠다.