졸업 작품/서버

C++ IOCP 서버 3. IOCP 구현 (작동부)

장형이 2019. 5. 16. 02:04

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

 

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

IOCP 초기화 : https://developstudy.tistory.com/45?category=836040

 

이번 포스팅에서는 드디어 IOCP를 끝맺는다!

저번 포스팅까지는 IOCP를 사용하기 위해 준비를 했다면 이제는 직접 관리(작동)하는 부분을 정리할 것이다.

 

비동기 명령을 보내는 함수는 Post___()로 지었고, 결과를 받는 함수는 ____Proc(Overlapped*)로 지었으니 참고 바란다.

 

구현해야 할 명령은 총 4개이다.

Send, Recv, Close, Accpet

 

1. PostSend (비동기 송신 요청)

void CIOCPServer::PostSend(const std::shared_ptr<CPlayer>& player, const char* buf, const int& sendLen, bool isTCP)
{
	FSendOverlapped* overlap = _sendOverlapObjects->PopObject();

	// 앞에 총 길이를 더해서 보낸다.
	int realSendLen = sendLen + sizeof(int);

	// 버퍼를 만든다.
	MySerializer::IntSerialize(overlap->wsabuf.buf, sendLen);
	memcpy(overlap->buf + sizeof(int), buf, sendLen);
	overlap->wsabuf.len = realSendLen;

	// TCP 플래그거나, Player의 UDP 주소가 없으면 WSASend를 수행한다.
	if (isTCP || player->udpAddr == nullptr) {
		if (WSASend(player->socket->GetSocket(), &overlap->wsabuf, 1, &overlap->wsabuf.len, 0, overlap, NULL) == SOCKET_ERROR)		// 데이터 전송
		{
			if (WSAGetLastError() != WSA_IO_PENDING)
			{
				ServerNetworkSystem->CloseConnection(player);
				WriteLog(ELogLevel::Warning, CLog::Format("Failed to WSASend. Error : ", WSAGetLastError()));
				return;
			}
		}
	}
	// UDP는 WSASendTo를 사용하여 전송한다.
	else {
		if (WSASendTo(_udpRecvSocket,
			&overlap->wsabuf, 1,
			&overlap->wsabuf.len, 0,
			(FSocketAddr*)player->udpAddr.get(), sizeof(FSocketAddrIn),
			overlap, NULL) == SOCKET_ERROR)		// 데이터 전송
		{
			if (WSAGetLastError() != WSA_IO_PENDING)
			{
				ServerNetworkSystem->CloseConnection(player);
				WriteLog(ELogLevel::Warning, CLog::Format("Failed to WSASendTo Error : ", WSAGetLastError()));
				return;
			}
		}
	}
}

나는 Send를 보낼 때마다 버퍼 앞에 길이를 담는 int를 붙여서 보내기로 했다.

int를 직렬화해서 앞에다 복사하고, 뒤에 송신할 버퍼를 만들었다.

그다음 TCP 거나 UDP 주소가 없다면 WSASend()를 사용하여 해당 플레이어의 소켓에다가 송신하였다.

UDP의 경우에는 WSASendTo()를 사용하여 Addr에 직접 보냈다.

 

2. SendProc (비동기 송신 완료)

void CIOCPServer::_SendProc(FSendOverlapped* overlap)
{
	_sendOverlapObjects->ReturnObject(overlap);
}

비동기 송신이 완료되면 Overlapped를 수거하기만 하면 된다.

 

3. PostRecv (비동기 수신 요청)

void CIOCPServer::PostRecv(FBuffuerableOverlapped* overlap, bool isTCP)
{
	DWORD recvBytes;
	DWORD flags = 0;
	if (isTCP) {
		if (!overlap) return;
		FTCPRecvOverlapped* tcpOverlap = (FTCPRecvOverlapped*)overlap;

		tcpOverlap->wsabuf.len = PACKET_BUFSIZE;

		// TCP는 WSARecv()를 사용하여 플레이어의 소켓에 Recv를 요청한다.
		int retval = WSARecv(tcpOverlap->m_player->socket->GetSocket(), &tcpOverlap->wsabuf, 1,
			&recvBytes, &flags, tcpOverlap, NULL);
		if (retval == SOCKET_ERROR) {
			if (WSAGetLastError() != WSA_IO_PENDING) {
				WriteLog(ELogLevel::Warning, CLog::Format("WSARecv Error %d", WSAGetLastError()));

				// Close and logging
				ServerNetworkSystem->CloseConnection(tcpOverlap->m_player);
				return;
			}
		}
	}
	else {
		FUDPRecvOverlapped* udpOverlap = nullptr;
		if (overlap) udpOverlap = (FUDPRecvOverlapped*)overlap;

		if (udpOverlap == nullptr) {
			udpOverlap = _udpRecvOverlapObjects->PopObject();
			udpOverlap->wsabuf.len = PACKET_BUFSIZE;
		}

		// UDP는 WSARecvFrom을 사용하여, 플레이어의 Addr에 RecvFrom을 요청한다.
		udpOverlap->udpAddr = _udpServeraddr;
		int len = sizeof(udpOverlap->udpAddr);
		if (WSARecvFrom(_udpRecvSocket, &udpOverlap->wsabuf, 1,
			&recvBytes, &flags, (sockaddr*)&udpOverlap->udpAddr, &len, udpOverlap, NULL) == SOCKET_ERROR) {
			if (WSAGetLastError() != WSA_IO_PENDING) {
				WriteLog(ELogLevel::Warning, CLog::Format("WSARecvFrom Error %d", WSAGetLastError()));
				PostRecv(false, (FBuffuerableOverlapped*)udpOverlap);
				return;
			}
		}
	}
}

수신은 완료가 됐을 때 다시 수신을 반복하므로 overlapped를 만드는 행위는 처음에 딱 한 번만 하면 된다.

그래서 인자로 받도록 구현하였다.

TCP는 WSASend와 쌍을 이루는 WSARecv로 구현,

UDP는 WSASendTo와 쌍을 이루는 WSARecvFrom으로 구현하였다.

 

4. RecvProc (비동기 수신 완료)

void CIOCPServer::_RecvProc(FBuffuerableOverlapped* overlap, const int& len, bool isTCP)
{
	if (isTCP) {
		FTCPRecvOverlapped* tcpOverlap = (FTCPRecvOverlapped*)overlap;
		// TCP 수신 데이터를 까서 로직을 처리하는 클래스에 넘긴다.
		if (!TCPReceiveProcessor->ReceiveData(tcpOverlap->m_player, tcpOverlap->buf, len)) {
			std::string errorLog = CLog::Format("[TCP ReceiveData Error] %s\n", inet_ntoa(tcpOverlap->m_player->addr.sin_addr));
			printf_s("%s", errorLog.c_str());
			WriteLog(ELogLevel::Error, errorLog);
			ServerNetworkSystem->CloseConnection(tcpOverlap->m_player);
		}
		else PostRecv(overlap, true);
	}
	else {
		FUDPRecvOverlapped* udpOverlap = (FUDPRecvOverlapped*)overlap;
		// UDP 수신 데이터를 까서 로직을 처리하는 클래스에 넘긴다.
		if (!UDPReceiveProcessor->ReceiveData(udpOverlap->udpAddr, udpOverlap->buf, len)) {
			std::string errorLog = CLog::Format("[UDP ReceiveData Error] %s\n", inet_ntoa(udpOverlap->udpAddr.sin_addr));
			printf_s("%s", errorLog.c_str());
			WriteLog(ELogLevel::Error, errorLog);
		}
		// UDP는 오류가나도 계속 진행한다.
		PostRecv(overlap, false);
	}
}

수신이 완료되면 데이터를 해석하는 클래스에 넘긴다.

이후 수신이 성공했다면 다시 똑같은 소켓에 Recv를 반복하도록 하였다.

UDP는 하나의 소켓으로 반복하므로 수신이 실패해도 계속 반복하도록 하였다.

 

5. PostClose (비동기 단절)

void CIOCPServer::PostClose(CSocket* socket)
{
	if (!socket || !socket->GetSocket() || sockStates[socket] == false) return;

	// 같은 소켓을 또다시 단절하는 일이 없도록 체크한다.
	sockStates[socket] = false;

	FCloseOverlapped* overlap = _closeOverlapObjects->PopObject();
	overlap->socket = socket;

	// 단절을 요청한다.
	if (TransmitFile(overlap->socket->GetSocket(),
		0, 0, 0, overlap, 0, TF_DISCONNECT | TF_REUSE_SOCKET) == FALSE)
	{
		DWORD error = WSAGetLastError();
		if (error != WSA_IO_PENDING)
		{
			_socketObjects->ReturnObject(socket);
			_closeOverlapObjects->ReturnObject(overlap);
			WriteLog(ELogLevel::Warning, CLog::Format("Failed to close. Error : ", WSAGetLastError()));
			return;
		}
	}
}

이미 닫았던 소켓을 또 닫는 현상이 자주 있어서 STL Map을 사용해서 윗단에서 한번 걸러주도록 구현하였다.

단절을 비동기 요청하는 함수는 TransmitFile을 쓰면 된다.

소켓의 핸들을 끊는 플래그 TF_DISCONNECT소켓을 다시 사용할 것이라고 알리는 TF_REUSE_SOCKET을 옵션으로 넣어줬다.

 

6. CloseProc (비동기 단절 완료)

void CIOCPServer::_CloseProc(FCloseOverlapped* overlap)
{
	// 오브젝트를 반납한다.
	_socketObjects->ReturnObject(overlap->socket);
	_closeOverlapObjects->ReturnObject(overlap);
}

단절 완료도 간단하다.

사용한 overlapped와 소켓을 수거하도록 하면 끝!!

 

7. PostAccept (비동기 접속)

void CIOCPServer::PostAccept()
{
	FAcceptOverlapped* overlap = _acceptOverlapObjects->PopObject();
	CSocket* newSocket = _socketObjects->PopObject();

	overlap->socket = newSocket;

	WriteLog(ELogLevel::Warning, "Start accept.");

	// accept
	if (AcceptEx(_tcpListenSocket->GetSocket(), newSocket->GetSocket(), (LPVOID)&(overlap->m_acceptBuf), 0,
		sizeof(overlap->m_acceptBuf.m_pLocal), sizeof(overlap->m_acceptBuf.m_pRemote), &_tcpAcceptLen, overlap) == FALSE) {
		if (WSAGetLastError() != WSA_IO_PENDING)
		{
			PostClose(newSocket);
			WriteLog(ELogLevel::Warning, CLog::Format("Failed to accept. Error : ", WSAGetLastError()));
			_bIsRun = false;
		}
	}
}

Accept는 새로운 소켓을 만들어서 시도해야 하므로 socket objectPool에서 꺼내서 AcceptOverlapped에 연결해줬다.

비동기 Accept를 요청하는 함수는 AcceptEx이며, 특이하게 Accept가 끝나고 리턴되는 엔드포인트 값을 넣는 포인터와 크기를 같이 넘겨줘야 한다.

 

8. AcceptProc (비동기 접속 완료)

void CIOCPServer::_AcceptProc(FAcceptOverlapped * overlap)
{
	// 소켓 옵션을 활성화 상태로 바꾼다.
	SOCKET listenSocket = _tcpListenSocket->GetSocket();

	if (setsockopt(overlap->socket->GetSocket(), SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
		(const char *)&listenSocket, sizeof(SOCKET)) == SOCKET_ERROR)
	{
		std::string clientAcceptLog = CLog::Format("Can't set update accept context sockopt in _AcceptProc.");
		WriteLog(ELogLevel::Warning, clientAcceptLog);
		PostClose(overlap->socket);
		_acceptOverlapObjects->ReturnObject(overlap);
		return;
	}

	// Accept한 소켓의 addr을 받아온다.
	FSocketAddr *local_addr, *remote_addr;
	int l_len = 0, r_len = 0;
	GetAcceptExSockaddrs(&overlap->m_acceptBuf, 0, sizeof(overlap->m_acceptBuf.m_pLocal),
		sizeof(overlap->m_acceptBuf.m_pRemote), &local_addr, &l_len, &remote_addr, &r_len);
	FSocketAddrIn clientaddr = *(ADDRToADDRIN(remote_addr));

	std::string clientAcceptLog = CLog::Format("[Accept] IP=%s, PORT=%d",
		inet_ntoa(clientaddr.sin_addr),
		ntohs(clientaddr.sin_port));
	WriteLog(ELogLevel::Warning, clientAcceptLog);

	// 플레이어를 생성한다.
	int addrlen = sizeof(clientaddr);
	std::shared_ptr<CPlayer> newPlayer(new CPlayer());
	newPlayer->socket = overlap->socket;
	newPlayer->steamID = 0;
	newPlayer->lastPingTime = std::chrono::system_clock::now();
	newPlayer->lastPongTime = std::chrono::system_clock::now();
	newPlayer->state = LOBBY;
	newPlayer->udpAddr = nullptr;
	newPlayer->addr = clientaddr;
	PlayerManager->AddPlayer(newPlayer);
	sockStates[newPlayer->socket] = true;
	_acceptOverlapObjects->ReturnObject(overlap);
    
	// 새로운 방을 만들어서 플레이어에게 할당한다.
	RoomManager->CreateRoom(newPlayer);

	// 수신을 위한 overlapped를 준비한다.
	FTCPRecvOverlapped* recvOverlap = new FTCPRecvOverlapped(newPlayer);
	recvOverlap->wsabuf.len = PACKET_BUFSIZE;

	// IOCP를 연결해준다.
	CreateIoCompletionPort((HANDLE)newPlayer->socket->GetSocket(), hcp, newPlayer->socket->GetSocket(), 0);

	// Recv를 실행한다.
	DWORD recvbytes, flags = 0;
	if (WSARecv(newPlayer->socket->GetSocket(), &recvOverlap->wsabuf, 1, &recvbytes,
		&flags, recvOverlap, NULL) == SOCKET_ERROR) {
		if (WSAGetLastError() != ERROR_IO_PENDING) {
			WriteLog(ELogLevel::Error, CLog::Format("Failed to recv : %d", WSAGetLastError()));
		}
	}

	// 다시 Accept을 준비한다.
	PostAccept();
}

먼저 Accept에 사용한 소켓을 활성화 상태로 바꾸는 옵션(SO_UPDATE_ACCEPT_CONTEXT)을 적용해준다.

이후 GetAcceptExSockaddrs 함수를 사용하여 Addr을 받아 올 수 있다.

이 후 정보들을 토대로 Player를 만들어주고, Player들을 묶는 클래스 Room에 속하게 해 준다.

 

이후 Player의 소켓을 IOCP에 연결해주고, 비동기 Recv를 시작한다.

이어서 비동기 Accept를 다시 준비하면 끝!

 

9. 완료!

 

이제 모든 함수가 완성됐다.

처음으로 돌아가 초기화 이후 몇 가지만 더 해주면 된다.

 

bool CIOCPServer::Run()
{
	if (IsRun()) return false;
	_bIsRun = true;

	// Init WSA
	WSAData wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0 || !InitIOCP()) return false;

	// 초기화
	if (!InitObjectPools() || !InitTCPListenSocket() || !InitWorkerThread() || !InitUDPListenSocket()) {
		_bIsRun = false;
		return false;
	}

	// 첫 TCP Accept 시작
	PostAccept();

	// 첫 UDP Recv 시작
	PostRecv(nullptr, false);

	return true;
}

초기화를 끝내고 나서 첫 TCP Accept와 첫 UDP Recv를 실행하는 함수를 추가해주면 정말 끝!

 

후기

 

IOCP를 학습하면서 예제를 찾아보면 에코 서버 말고는 잘 안 나와서 매우 힘들었다.. ㅠㅜ

하지만 그래도 어떻게든 (잘 작동하는지 사실상 확신은 안 들지만) 완성해서 너무 기쁘다.

사실 이 부분 말고도 할게 산더미라 걱정도 많이 들지만 나는 해낼 것이다!!