IOCP 게임서버

IOCP 실습 1 Echo Server 만들기

묭묭.cpp 2023. 8. 28. 02:50

현재 대학 졸업작품으로 IOCP 모델을 사용하여 게임서버를 만들려는 중 어떻게 갈피를 잡고 프로그래밍을 해야하는지 어려움을 겪었다.
그래서 유튜브 강좌 등 찾아보던 도중 최흥배 님의 단계별 실습을 통해 IOCP 배우기라는 영상을 보고 천천히 따라서 작성해보려고 한다.


#출처 https://www.youtube.com/watch?v=RMRsvll7hrM

1. IOCP 학습

1. 비유를 통한 IOCP 이해하기

  1. 은행원이 1명 있음 -> 여러명의 고객을 응대
  2. 통장을 개설하는 작업을 수행한다고 하자
  3. 은행원은 고객에게 통장의 기본 정보를 작성하도록 시킨다.
  4. 고객이 위 작업을 수행하는 동안 은행원은 다음 고객에게도 똑같은 작업을 시킨다.
  5. 은행원은 고객의 작업이 완료되는대로 다른 일을 부여하거나 완료처리한다.
  • 위 예시에서 은행원은 CPU, 고객은 소켓이다.
  • 이것이 바로 IOCP의 기본 골격
  • IOCP는 사실 멀티쓰레드 환경에서 동작하는 것이 아닌 Smart한 쓰레드 하나를 두고서 여러개의 클라이언트 요청을 처리하는 모델

2. IOCP 모델의 동작과정

  1. IOCP 오브젝트 생성 : CreateIoCompletionPort
  2. 서버에서 클라이언트의 연결요청 수락 -> 클라이언트 소켓 생성
  3. 클라이언트 소켓을 IOCP 오브젝트에 등록 : CreateIoCompletionPort
  4. 소켓을 대상으로 입출력을 수행
  5. 완료 정보가 IOCP 오브젝트에 등록됨
  6. IOCP 오브젝트에 등록된 완료정보를 확인 : GetQueuedCompletionStatus
  7. 확인 이후에 입출력 작업 (해야하는 일) 진행
  • 입출력 업무를 수행할 쓰레드 : CPU 코어 1개 당 1개의 쓰레드

3. IOCP 핵심 함수의 기능

CreateIOCompletionPort

1. IOCP 커널 오브젝트 생성

HANDLE hCpObject;
hCpObject = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);
  • IOCP 커널 오브젝트를 생성한다.
  • 4번째 인자 : IOCP 오브젝트에 IO 이후에 작업할 쓰레드의 개수를 전달한다.

여기서 4번째 인자의 의미

IOCP 오브젝트를 생성할 때 자동으로 쓰레드가 생성되는 것이 아니다.

여기서 2라는 숫자의 의미는 IOCP 오브젝트에 할당가능 한 쓰레드의 수를 2개로 제한한다는 것이다.

즉, GetQueuedCompletionStatus 함수를 동시에 호출할 수 있는 쓰레드의 수를 2개로 제한한다는 것

결국 2개의 쓰레드만 IOCP 오브젝트에서 완료 정보를 받아 일할 수 있다는 것이다!

 

2. IOCP 커널 오브젝트에 대상 소켓을 등록

CreateIoCompletionPort((HANDLE)hSock, hCpObject, (DWORD)ioInfo, 0);
  • 두번 째 인자(IOCP 오브젝트)에 첫번 째 인자(클라이언트 소켓)를 등록한다.
  • 클라이언트 소켓의 완료 정보를 IOCP 오브젝트로부터 받아올 수 있게 등록하는 것!

GetQueuedCompletionStatus

BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMiliseconds);
  • 위 함수로 등록한 소켓의 완료정보를 획득할 수 있다.
  • 반환값이 true일 때 각각에 해당하는 작업을 수행하면 된다.
  • lpOverlapped 구조체에 Event 정보를 등록하고 송수신일 경우 SEND / RECV 처리를 구분하여 수행하면 된다.

추가적으로...

windows 소켓 프로그래밍의 accept 함수는 blocking 방식의 소켓 함수이다.

한 쓰레드에서 모든 작업을 수행하게 되면 accept의 응답이 없을 때 그 부분에서 코드가 멈춰버린다.

그렇기 때문에 accept 전용 쓰레드를 두고 나머지 작업은 다른 쓰레드에서 수행하도록 하는 것이 바람직스럽다.

 

IOCP를 한줄로 요약하자면

IO의 완료 정보를 통지받아 수행하자~ 는 것이다.

 

위 내용으로 IOCP에 대한 전반적인 이해는 마무리하였다.

막상 두려워하던 IOCP 였지만 차근차근 짚어보니 그렇게 개념적인 부분은 어렵지 않았다.

이제 최흥배님의 IOCP 실습 예제를 1단계 씩 풀어볼 예정이다.

 

2. IOCP Echo Server 구현하기

1단계로는 IOCP Echo Server를 간단하게? 구현해보는 것이다.

1단계 예제는 처음부터 짜려면 조금 어려울 수 있으니 예제 코드를 보고 구현하고 분석해본 뒤 안보고 짜보는 것을 추천한다고 유튜브 설명 강좌에서 말하셨다.

그러므로 나또한 코드를 보고 분석해보았다.

1. enum class와 구조체

enum class IOOperation
{
	RECV,
	SEND
};

struct stOverlappedEx
{
	WSAOVERLAPPED   m_wsaOverlapped;
	SOCKET	        m_socketClient;			
	WSABUF	        m_wsaBuf;		
	char	        m_szBuf[ MAX_SOCKBUF ]; 
	IOOperation     m_eOperation;			
};	
	
struct stClientInfo
{
	SOCKET		m_socketClient;			
	stOverlappedEx	m_stRecvOverlappedEx;	
	stOverlappedEx	m_stSendOverlappedEx;	
	
	stClientInfo()
	{
		ZeroMemory( &m_stRecvOverlappedEx , sizeof( stOverlappedEx ) );
		ZeroMemory( &m_stSendOverlappedEx , sizeof( stOverlappedEx ) );
		m_socketClient = INVALID_SOCKET;
	}
};

 

enum class IOOperation

  • enum class를 활용하여 IO Event 정보를 선언하였다.
  • 이를 활용하여 stOverlappedEx 구조체에 동작 종류를 담아주고 이 값을 비교하여 수행할 작업을 구분한다.

stOverlappedEx 구조체

  • WSAOVERLAPPED 구조체를 확장하여 필요한 정보를 담았다.
  • 클라이언트 소켓, 데이터 버퍼, 동작 종류 정보를 추가로 담고있다.

stClientInfo 구조체

  • 클라이언트 소켓 정보, Overlapped IO 작업을 위한 변수를 가지고 있다.
  • 생성자에서 기본 세팅을 해준다.

 

2. IOCP Echo Server의 동작과정

코드로 작성하기 전 동작 흐름을 정리해보았다.

동작과정은 생각보다 간단하다.

  1. 소켓 초기화
  2. bind
  3. listen
  4. IOCP 오브젝트 생성

Accept 쓰레드에서

  1. 접속 승인 (클라이언트 소켓 생성)
  2. IOCP 오브젝트에 클라이언트 소켓 등록
  3. RECV 작업 요청

Woker 쓰레드에서

  1. RECV 작업 완료 (클라이언트에서 SEND)
  2. RECV 작업 후 수신 데이터 출력
  3. 클라이언트에 SEND 작업 수행 (Echo)
  4. 다시 RECV 작업 요청
  5. 1 - 4 과정을 반복

위의 과정을 코드로 구현하면 된다.

3. IOCompletionPort Class의 구현

1. InitSocket() 함수

  1. WSAStartup 함수 호출
  2. WSASocket 함수 호출

2가지 함수를 호출하여 소켓을 초기화 한다.

Listen Socket의 생성까지 담당하고 있다.

 

2. BindandListen() 함수

  1. 서버의 주소정보를 SOCKADDR_IN 구조체에 기입
  2. listen 소켓과 주소정보를 bind
  3. 접속 요청을 받기 위해 listen 함수 호출

클라이언트의 접속을 받기 위한 과정을 수행한다.

 

3. StartServer() 함수

  1. IOCP 핸들을 생성 : CreateIoCompletionPort
  2. WokerThread 활성화 - IO 작업 수행
  3. AccepterThread 활성화 - Accept 작업 수행

이 함수에서 IOCP 핸들을 생성하고 각 쓰레드를 활성화한다.

각 쓰레드의 main 코드의 동작과정은 이어서 설명할 예정이다.

 

4. WokerThread의 main 함수

  1. GetQueuedCompletionStatus() 함수로 IO의 완료정보를 받아옴
  2. 만약 완료라면?
  3. stOverlappedEx의 IOOperation 값을 비교하여 이후 작업을 수행한다.
  4. IOOperation의 값이 RECV라면?
  5. stOverlappedEx 구조체의 버퍼를 확인하여 출력
  6. 받은 버퍼를 그대로 송신 (Echo)
  7. IOOperation의 값이 SEND라면?
  8. 송신한 내용을 출력

IO의 완료정보를 받아서 IO 작업을 수행한다.

stOverlappedEx 구조체의 기입된 정보를 바탕으로 작업을 진행한다.

버퍼 정보, 소켓 정보, IOOperation...

5. AccepterThread의 main 함수

  1. accept 함수 호출
  2. 클라이언트가 접속 되었다면?
  3. IOCP 오브젝트에 클라이언트 소켓 등록
  4. Recv Overlapped IO 작업 요청

accept 함수 호출로 클라이언트 접속을 받는 작업을 진행한 후

IOCP 오브젝트에 클라이언트 소켓을 등록한다.

이어서 Recv 작업을 요청하므로 클라이언트와 통신을 진행할 준비를 마치는 작업까지 담당한다.

 

함수의 흐름을 서술하자면 여기까지지만,,,,

물론 IOCP 오브젝트에서 완료 통지를 받으려면 Overlapped 구조체에 정보를 세팅하고 Recv, Send 하여야 한다.

 

이로써 IOCP Server 실습 1단계를 마무리하도록 하겠다.

IOCP를 이해하고 코드 분석을 하니 생각보다 쉬운? 내용이어서 흥미로웠다.

개념 학습에서는 윤성우 - 열혈 TCP/IP 소켓 프로그래밍 책을 보고 학습하였는데

해당 책에서 나온 IOCP의 흐름과 그대로 구현되어 있어서 이해하기 한결 수월했다.

 

코드를 삽입하고 이를 설명하는 것보다는 코드의 흐름을 이해하고 그 내용을 적는 것이 학습에 더 도움이 되지 않을까 싶어서 코드를 직접 포스팅하지는 않았는데 어떤 방식이 더 효율적인지는 잘 모르겠다.

그래도 나중에 이 포스팅을 보고 다시 서버 프로그램을 작성한다면 내가 정리한 흐름을 보고 다시 작성할 수 있지 않을까라고 예상한다.

 

나중에 참고용으로 보기위하여 깃허브 풀코드 링크 정도만 남기고 포스팅을 마치겠다.

https://github.com/MyungHyun-Ahn/Server-Programming-Study/tree/main/IOCP_STUDY/IOCP_01

 

잘못된 내용이나 문의사항이 있다면 댓글부탁드립니다.

 

반응형