나만의 작은 도서관

[TIL][C++] 250507 MMO 서버 개발 13일차: Accept이벤트를 미리 걸어준다면 어떤 정보를 미리 넘겨줘야 할까?, 객체 생성과 초기화의 분리, 매크로의 매크로, 등등... 본문

Today I Learn

[TIL][C++] 250507 MMO 서버 개발 13일차: Accept이벤트를 미리 걸어준다면 어떤 정보를 미리 넘겨줘야 할까?, 객체 생성과 초기화의 분리, 매크로의 매크로, 등등...

pledge24 2025. 5. 8. 01:00
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

Accept이벤트를 미리 걸어준다면 어떤 정보를 미리 넘겨줘야 할까?

  • 현재 사용하고 있는 Accept이벤트는 동기적으로 새로운 연결이 감지될 때까지 기다리지 않고, 미리 등록한 Accept 이벤트를 CP의 완료 패킷을 통해 통지받는다. 그렇다면 GQCS를 통해 완료 패킷을 받은 경우 어떠한 정보를 알고 있어야 할까?
  • Accept를 미리 등록한 스레드와 Accept 이벤트에 대한 완료 패킷을 받은 스레드는 다르다(운이 좋으면 같을 수도 있다). 그래서 등록 시에 미리 넘겨줘야 하는 정보들이 있는데, 다른 네트워크 이벤트가 그랬듯이 OVERLAPPED 구조체를 확장하여 추가 정보를 넘겨주는 방식을 사용한다.
  • 원점으로 돌아와서 결국 어떤 정보를 알고 있어야 하는가인데… 세션에 대한 정보만 알고 있으면 될 것으로 보인다. 소켓이나, 현재 서비스 타입 등에 대한 정보를 전부 세션이 들고 있으면 Accept 완료 패킷에 대한 처리를 하는 데 있어 모자람이 없을 것이다. 따라서, 세션에 1) 소켓 정보, 2) 서비스 타입 등을 넣고, 이를 넘겨받도록 한다.
class AcceptEvent : public WSAOVERLAPPED
{
...
private:
	SessionRef _session;
}

class Session 
{
...
private:
	ServiceRef _service; // weak_ptr로 들고 있어도 된다.
	SOCKET _socket
}

객체 생성과 초기화의 분리

  • 객체 생성 시 생성자를 통해 초기화를 동시에 할 수 있다. 여기서 고려해야 할 점은 초기값이 외부값에 의존하는가?이다.
class Session_AType
{
public:
	Session_Atype();
	Session_Atype(T t1, U t2); // 1) 생성자를 통한 초기화
	void Init(T t1, U t2...);  // 2) 함수를 통한 초기화
private:
	T _t1;
	U _t2;
	X _t3 = SOME_ENUM;
}
    1. 번 방식과 2) 방식을 동시에 사용해도 문제는 없겠지만, 하나의 객체를 초기화하는데 여러 방식을 두고 싶지 않아 하나의 방식을 선택하고 싶었다. 두 방식은 ‘초기화가 되지 않은 객체를 미리 만들 수 있는가?’에 차이가 있는데, 미리 만드는 것이 가능해지면 보다 자유롭게 객체를 만들 수 있다는 장점이 있지만, 빈 객체를 불러와 사용할 수 있다는 실수의 여지가 있다는 단점이 있다.
  • 최적화적인 부분도 있겠지만, 무엇이 더 편한가? 를 생각했을 때는 ObjectPool을 사용할 경우 2) 번 방식이 용이하므로 2번 방식을 사용할 것 같다.

매크로의 매크로

  • winsock 라이브러리를 보면 아래와 같이 같은 역할이지만 다른 이름을 붙이기 위해 매크로를 아래와 같이 사용한다.
#define WSA_IO_PENDING            (ERROR_IO_PENDING)
#define (ERROR_IO_PENDING)        997L

WSA_IO_PENDING과 완료 패킷

  • WSARecv이든, WSASend이든, AccepteEx이든 결과가 CP의 완료 패킷으로 들어가는 I/O 함수들은 성공적으로 네트워크 이벤트가 등록될 경우 반드시 CP에 완료 패킷으로 들어간다. (그 결과가 성공이든 실패이든)
  • 하지만, 등록에 실패할 경우 오류가 발생하며 리턴값이 true가 아닌 false가 반환된다. 이 경우 CP에 해당 네트워크 이벤트에 대한 완료 패킷은 들어가지 않는다.
  • 그런데 여기서 예외가 있다. I/O 함수의 반환값이 false여도 완료 패킷이 CP에 들어가는 경우가 있는데, WSA_IO_PENDING 오류가 발생했을 때가 그렇다.
  • WSA_IO_PENDING 오류는 실패가 아닌, 등록이 진행 중임을 의미한다. 즉, 아직 등록이 되지 않아 false가 반환되었지만, 그 결과가 실패했음을 의미하진 않는다. 하지만, 성공을 의미하지도 않기 때문에, WSA_IO_PENDING이 뜬 경우 완료 패킷을 통해 성공 여부를 확인해야 한다.
if(true == WSASend())
{
...
}
else
{
	int errorCode = WSAGetLastError();
	if(erroeCode == WSA_IO_PENDING) /* 아직 실패 및 성공 여부를 모름 */
	else /* 진짜 오류임 */
	{
	
	}
}

if(GQCS())
else /* 완료 패킷이 false */
{
	if(overlapped != null) /* 등록한 이벤트의 실패 */
	{
		int errorCode = WSAGetLastError(); // WSA_IO_PENDING 이후 실패 시 확인
	...
	}
	else /* GQCS 자체의 실패(시스템 오류)*/
	{
	...
	}
}

+) 추가 내용

  • IOCP 소켓 모델을 사용하는 경우, 높은 확률로 워커 스레드를 여러 개 배치하는 멀티스레드 환경일 텐데, 당연하다는 듯이 소켓의 마지막 에러를 반환하는 WSAGetLastError()를 현재 소켓의 에러라고 생각하고 사용하고 있다.
  • 이처럼 사용할 수 있는 이유는 WSAGetLastError()의 오류값은 스레드에 종속되며, 걸어둔 GQCS가 반환될 경우 현재 할당받은 완료 패킷이 오류가 발생했을 때 갱신된다. 따라서, 멀티스레드 환경임에도 WSAGetLastError의 오류값은 다른 스레드로부터 영향을 받지 않는다.

GQCS 함수가 false 반환 시 정리표

GQCS overlapped 구조체 WSAGetLastError()

실패한 네트워크 이벤트(PENDING) false 있음 해당 I/O 에러
GQCS자체의 실패(큐 문제 등) false NULL 시스템 오류

completion key로 session을 넣어주었을 때 문제점

  • CICP의 comletion key로 session 주소를 넣는 방식은 약간의 문제점이 있다. CICP에 completon key를 넣을 때 생포인터를 인자로 받기 때문에 스마트 포인터를 사용할 수 없다는 것이다. (정확히는 스마트 포인터의 refCount 관리를 받지 못한다.) 따라서, reCount를 사용하고 싶은 경우 refCount를 관리하는 클래스를 정의 및 상속받아 session을 넘겨줘야 할 것이다.
  • 하지만 이런다고 문제가 완전히 해결된 것은 아니다. session이 custom 한 refCount를 사용했기 때문에 역으로 스마트 포인터로 관리할 수가 없어진다(양쪽으로 refCount를 관리하기 때문)
  • 따라서, 그냥 overlapped 구조체를 확장하여 데이터를 가지고 있는 것이 깔끔하다!

shared_ptr과 동적 할당

  • shared_ptr로 관리하는 객체는 정적 할당하여 스택에서 들고 있으면 안 되고, 반드시 동적 할당으로 들고 있어야 한다.