본문 바로가기
개인공부/IOCP 서버 제작 실습

IOCP 서버 제작 실습 2

by 하고싶은건많은놈 2023. 3. 21.

기존에는 데이터 버퍼가 Overlapped 구조체 확장형에 존재(m_szBuf)

  • 비동기 I/O 요청시마다 Overlapped 구조체가 생성됨
  • 그러나 버퍼 크기보다 데이터가 작을 수 있고, 버퍼를 사용하지 않는 I/O 요청이 있을 수도 있음
  • 따라서 현재 구조로는 불필요한 메모리 낭비가 발생할 수 있음
struct stOverlappedEx
{
	WSAOVERLAPPED	m_wsaOverlapped;		//OVERLAPPED 구조체
	SOCKET			m_socketClient;			//클라이언트 소켓
	WSABUF			m_wsaBuf;				//Overlapped I/O 작업 버퍼
	char			m_szBuf[MAX_SOCKBUF];	//데이터 버퍼
	IOOperation		m_eOperation;			//작업 동작 종류
};

 

 

따라서 데이터 버퍼를 클라이언트 객체로 옮기는 작업을 진행

또한 recv 버퍼와 send 버퍼를 나누어 에코 도중 추가로 데이터를 주고받을 때도 정상적으로 작동하도록 구성

struct stClientInfo
{
	SOCKET			m_socketClient;			//Client 소켓
	stOverlappedEx	m_stRecvOverlappedEx;	//Recv Overlapped I/O 작업 위한 변수
	stOverlappedEx	m_stSendOverlappedEx;	//Send OVerlapped I/O 작업 위한 변수

	char			mRecvBuf[MAX_SOCKBUF];	//데이터 버퍼(recv)
	char			mSendBuf[MAX_SOCKBUF];  //데이터 버퍼(send)

	stClientInfo()
	{
		ZeroMemory(&m_stRecvOverlappedEx, sizeof(stOverlappedEx));
		ZeroMemory(&m_stSendOverlappedEx, sizeof(stOverlappedEx));
		m_socketClient = INVALID_SOCKET;
	}
};

또한 해당 구조체들과 enum을 Define.h 파일로 따로 분리하도록 구조 수정

 



 

기존 코드에서 애플리케이션과 네트워크 코드를 분리

네트워크를 구성하는 코드는 IOCPServer.h에, 애플리케이션을 구성하는 코드는 EchoServer.h에 위치하도록 구성

애플리케이션은 서버의 클라이언트 연결 / 해제 / 데이터 수신시 출력되도록 구성

 

  • main.cpp
#include "EchoServer.h"
#include <string>
#include <iostream>

const UINT16 SERVER_PORT = 11021;
const UINT16 MAX_CLIENT = 100;

int main()
{
	EchoServer server;

	//소켓 초기화
	server.InitSocket();

	//소켓 서버 등록
	server.BindandListen(SERVER_PORT);

	server.StartServer(MAX_CLIENT);

	std::cout << "아무 키나 누를 때까지 대기\n";
	while (true)
	{
		std::string inputCmd;
		std::getline(std::cin, inputCmd);

		if (inputCmd == "quit")
		{
			break;
		}
	}

	server.DestroyThread();
	return 0;
}
  • Define.h
#pragma once
#include <WinSock2.h>
#include <WS2tcpip.h>

#define MAX_SOCKBUF 1024
#define MAX_WORKERTHREAD 4

enum class IOOperation
{
	RECV,
	SEND
};

//WSAOVERLAPPED 구조체 확장
struct stOverlappedEx
{
	WSAOVERLAPPED	m_wsaOverlapped;		//OVERLAPPED 구조체
	SOCKET			m_socketClient;			//클라이언트 소켓
	WSABUF			m_wsaBuf;				//Overlapped I/O 작업 버퍼
	IOOperation		m_eOperation;			//작업 동작 종류
};

//클라이언트 정보 구조체
struct stClientInfo
{
	INT32			mIndex = 0;
	SOCKET			m_socketClient;			//Client 소켓
	stOverlappedEx	m_stRecvOverlappedEx;	//Recv Overlapped I/O 작업 위한 변수
	stOverlappedEx	m_stSendOverlappedEx;	//Send OVerlapped I/O 작업 위한 변수

	char			mRecvBuf[MAX_SOCKBUF];	//데이터 버퍼(recv)
	char			mSendBuf[MAX_SOCKBUF];  //데이터 버퍼(send)

	stClientInfo()
	{
		ZeroMemory(&m_stRecvOverlappedEx, sizeof(stOverlappedEx));
		ZeroMemory(&m_stSendOverlappedEx, sizeof(stOverlappedEx));
		m_socketClient = INVALID_SOCKET;
	}
};
  • EchoServer.h
#pragma once
#include "IOCPServer.h""

class EchoServer : public IOCPServer
{
	virtual void OnConnect(const UINT32 clientIndex_) override
	{
		std::cout << "[OnConnect] 클라이언트 : Index " << clientIndex_ << std::endl;
	}

	virtual void OnClose(const UINT32 clientIndex_) override
	{
		std::cout << "[OnClose] 클라이언트 : Index " << clientIndex_ << std::endl;
	}

	virtual void ONReceive(const UINT32 clientIndex_, const UINT32 size, char* pData_) override
	{
		std::cout << "[OnReceive] 클라이언트 : Index " << clientIndex_ << ", dataSize " << size_ << std::endl;
	}
};
  • IOCPServer.h
#pragma once
#pragma comment(lib, "ws2_32")

#include <thread>
#include <vector>
#include <iostream>

#include "Define.h"


class IOCPServer
{

private:
	// 클라이언트 정보 저장 구조체
	std::vector<stClientInfo>	mClientInfos;
	//리슨 소켓
	SOCKET						mListenSocket = INVALID_SOCKET;
	//접속중인 클라이언트 수
	int							mClientCnt = 0;
	//IO Worker 쓰레드
	std::vector<std::thread>	mIOWorkerThreads;
	//Accpet 쓰레드
	std::thread					mAccepterThread;
	//CompletionPort 객체 핸들
	HANDLE						mIOCPHandle = INVALID_HANDLE_VALUE;
	//작업 쓰레드 동작 플래그
	bool						mIsWorkerRun = true;
	//접속 쓰레드 동작 플래그
	bool						mIsAccepterRun = true;

	void CreateClient(const UINT32 maxClientCount)
	{
		for (UINT32 i = 0; i < maxClientCount; ++i)
		{
			mClientInfos.emplace_back();
			mClientInfos[i].mIndex = i;
		}
	}

	//worker 쓰레드 생성
	bool CreateWorkerThread()
	{
		for (int i = 0; i < MAX_WORKERTHREAD; i++)
		{
			mIOWorkerThreads.emplace_back([this]() {WorkerThread(); });
		}

		std::cout << "WorkerThread start...\n";
		return true;
	}

	//accept요청 처리 쓰레드 생성
	bool CreateAccepterThread()
	{
		mAccepterThread = std::thread([this]() {AccepterThread(); });

		std::cout << "AccepterThread start...\n";
		return true;
	}

	//사용하지 않는 클라이언트 정보 구조체 반환
	stClientInfo* GetEmptyClientInfo()
	{
		for (auto& client : mClientInfos)
		{
			if (INVALID_SOCKET == client.m_socketClient)
			{
				return &client;
			}
		}
	}

	//CompletionPort 객체와 소켓 연결
	bool BindIOCompletionPort(stClientInfo* pClientInfo)
	{
		auto hIOCP = CreateIoCompletionPort((HANDLE)pClientInfo->m_socketClient, mIOCPHandle, (ULONG_PTR)(pClientInfo), 0);

		if (hIOCP == NULL || hIOCP != mIOCPHandle)
		{
			std::cout << "CreateIoCompletionPort() Error : " << GetLastError() << std::endl;
			return false;
		}

		return true;
	}

	//WSARecv overlapped I/O
	bool BindRecv(stClientInfo* pClientInfo)
	{
		DWORD dwFlag = 0;
		DWORD dwRecvNumBytes = 0;

		pClientInfo->m_stRecvOverlappedEx.m_wsaBuf.len = MAX_SOCKBUF;
		pClientInfo->m_stRecvOverlappedEx.m_wsaBuf.buf = pClientInfo->mRecvBuf;
		pClientInfo->m_stRecvOverlappedEx.m_eOperation = IOOperation::RECV;

		if (WSARecv(pClientInfo->m_socketClient, &(pClientInfo->m_stRecvOverlappedEx.m_wsaBuf), 1,
			&dwRecvNumBytes, &dwFlag, (LPWSAOVERLAPPED) & (pClientInfo->m_stRecvOverlappedEx), NULL) == SOCKET_ERROR
			&& (WSAGetLastError() != ERROR_IO_PENDING))
		{
			std::cout << "WSARecv() Error : " << WSAGetLastError() << std::endl;
			return false;
		}
		return true;
	}

	//WSASend overlapped I/O
	bool SendMsg(stClientInfo* pClientInfo, char* pMsg, int nLen)
	{
		DWORD dwRecvNumBytes = 0;

		CopyMemory(pClientInfo->mSendBuf, pMsg, nLen);

		pClientInfo->m_stSendOverlappedEx.m_wsaBuf.len = nLen;
		pClientInfo->m_stSendOverlappedEx.m_wsaBuf.buf = pClientInfo->mSendBuf;
		pClientInfo->m_stSendOverlappedEx.m_eOperation = IOOperation::SEND;

		if (WSASend(pClientInfo->m_socketClient, &(pClientInfo->m_stSendOverlappedEx.m_wsaBuf), 1,
			&dwRecvNumBytes, 0, (LPWSAOVERLAPPED) & (pClientInfo->m_stSendOverlappedEx), NULL) == SOCKET_ERROR
			&& (WSAGetLastError() != ERROR_IO_PENDING))
		{
			std::cout << "WSASend() Error : " << WSAGetLastError() << std::endl;
			return false;
		}

		return true;
	}

	//worker Thread
	void WorkerThread()
	{
		stClientInfo*	pClientInfo = NULL;
		BOOL			bSuccess = TRUE;
		DWORD			dwIoSize = 0;
		LPOVERLAPPED	lpOverlapped = NULL;

		while (mIsWorkerRun)
		{
			bSuccess = GetQueuedCompletionStatus(mIOCPHandle, &dwIoSize, (PULONG_PTR)&pClientInfo, &lpOverlapped, INFINITE);

			//쓰레드 종료 메시지 처리
			if (bSuccess == TRUE && dwIoSize == 0 && lpOverlapped == NULL)
			{
				mIsWorkerRun = false;
				continue;
			}

			if (lpOverlapped == NULL)
			{
				continue;
			}

			// 클라이언트 접속 해제
			if (bSuccess == FALSE || (dwIoSize == 0 && bSuccess == TRUE))
			{
				std::cout << "socket " << (int)pClientInfo->m_socketClient << " disconnected\n";
				CloseSocket(pClientInfo);
				continue;
			}

			stOverlappedEx* pOverlappedEx = (stOverlappedEx*)lpOverlapped;

			//Overlapped I/O Recv 
			if (pOverlappedEx->m_eOperation == IOOperation::RECV)
			{
				onReceive(pClientInfo->mIndex, dwIoSize, pClientInfo->mRecvBuf);

				//echo
				SendMsg(pClientInfo, pClientInfo->mRecvBuf, dwIoSize);
				BindRecv(pClientInfo);
			}
			//Overlapped I/O Send
			else if (pOverlappedEx->m_eOperation == IOOperation::SEND)
			{
				std::cout << "Send Bytes : " << dwIoSize << ", msg : " << pClientInfo->mSendBuf << std::endl;
			}
			else
			{
				std::cout << "socket " << (int)pClientInfo->m_socketClient << " exception";
			}
		} 
	}

	//사용자 접속 쓰레드
	void AccepterThread()
	{
		SOCKADDR_IN		stClientAddr;
		int				nAddrLen = sizeof(SOCKADDR_IN);

		while (mIsAccepterRun)
		{
			stClientInfo* pClientInfo = GetEmptyClientInfo();
			if (pClientInfo == NULL)
			{
				std::cout << "Client Full Error\n";
				return;
			}

			pClientInfo->m_socketClient = accept(mListenSocket, (SOCKADDR*)&stClientAddr, &nAddrLen);
			if (pClientInfo->m_socketClient == INVALID_SOCKET)
			{
				continue;
			}

			//I/O Completion Port 객체와 소켓 연결
			if (BindIOCompletionPort(pClientInfo) == false)
			{
				return;
			}

			//Recv Overlapped I/O 작업 요청
			if (BindRecv(pClientInfo) == false)
			{
				return;
			}
			
			onConnect(pClientInfo->mIndex);

			++mClientCnt;
		}
	}

	//소켓 연결 종료
	void CloseSocket(stClientInfo* pClientInfo, bool bIsForce = false)
	{
		// SO_DONTLINGER로 설정
		struct linger stLinger = { 0, };

		// SO_LINGER, timeout = 0으로 설정하여 강제 종료, 데이터 손실이 있을 수 있음
		if (bIsForce == true)
		{
			stLinger.l_onoff = 1;
		}

		shutdown(pClientInfo->m_socketClient, SD_BOTH);
		
		setsockopt(pClientInfo->m_socketClient, SOL_SOCKET, SO_LINGER, (char*)&stLinger, sizeof(stLinger));

		closesocket(pClientInfo->m_socketClient);

		pClientInfo->m_socketClient = INVALID_SOCKET;

		onClose(pClientInfo->mIndex);
	}

public:
	IOCPServer(void) {}

	~IOCPServer(void)
	{
		WSACleanup();
	}

	virtual void onConnect(const UINT32 clientINdex_) {}
	virtual void onClose(const UINT32 clientINdex_) {}
	virtual void onReceive(const UINT32 clientINdex_, const UINT32 size_, char *pData_) {}

	//소켓 초기화
	bool InitSocket()
	{
		WSADATA wsaData;

		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		{
			std::cout << "WSAStartup() Error : " << WSAGetLastError() << std::endl;
			return false;
		}

		SOCKET mListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, NULL, WSA_FLAG_OVERLAPPED);

		if (INVALID_SOCKET == mListenSocket)
		{
			std::cout << "WSASocket() Error : " << WSAGetLastError() << std::endl;
			return false;
		}

		std::cout << "socket init success\n";
		return true;
	}

	bool BindandListen(int nBindPort)
	{
		SOCKADDR_IN			stServerAddr;
		stServerAddr.sin_family = AF_INET;
		stServerAddr.sin_port = htons(nBindPort);
		stServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);

		if (bind(mListenSocket, (SOCKADDR*)&stServerAddr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
		{
			std::cout << "bind() Error : " << WSAGetLastError() << std::endl;
			return false;
		}

		if (listen(mListenSocket, 5) == SOCKET_ERROR)
		{
			std::cout << "listen() Error : " << WSAGetLastError() << std::endl;
			return false;
		}

		std::cout << "server registration success\n";
		return true;
	}

	//접속 요청 수락, 메시지 처리
	bool StartServer(const UINT32 maxClientCount)
	{
		CreateClient(maxClientCount);

		mIOCPHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, MAX_WORKERTHREAD);
		if (mIOCPHandle == NULL)
		{
			std::cout << "CreateIoCompletionPort() Error : " << GetLastError() << std::endl;
			return false;
		}

		if (CreateWorkerThread() == false)
		{
			return false;
		}

		if (CreateAccepterThread() == false)
		{
			return false;
		}

		std::cout << "Server start\n";
		return true;
	}

	//생성되어있던 쓰레드 파괴
	void DestroyThread()
	{
		mIsWorkerRun = false;
		CloseHandle(mIOCPHandle);

		for (auto& th : mIOWorkerThreads)
		{
			if (th.joinable())
			{
				th.join();
			}
		}

		mIsAccepterRun = false;
		closesocket(mListenSocket);

		if (mAccepterThread.joinable())
		{
			mAccepterThread.join();
		}
	}
};

 

'개인공부 > IOCP 서버 제작 실습' 카테고리의 다른 글

IOCP 서버 제작 실습 6  (0) 2023.03.23
IOCP 서버 제작 실습 5  (0) 2023.03.23
IOCP 서버 제작 실습 4  (0) 2023.03.22
IOCP 서버 제작 실습 3  (0) 2023.03.22
IOCP 서버 제작 실습 1  (0) 2023.03.21

댓글