KoreanFoodie's Study

[C++ 게임 서버] 4-5. Session #2 본문

Game Dev/Game Server

[C++ 게임 서버] 4-5. Session #2

GoldGiver 2023. 12. 7. 14:20

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 4-5. Session #2

핵심 :

1. Session 구현에 있어, Send 는 나름 복잡한 구석이 있다.

2. 송신 버퍼를 어떻게 만들 것인지, 전송 시 여러 쓰레드가 Pending 상태로 놓였을 때, 순서가 반드시 보장되어야 하는지 등등을 생각하면서 구현해 보자.

이번 글에서는 이전에 만들었던 Session 관련 클래스에 내용을 덧붙여 보겠다.

조금 더 구체적으로 이야기하자면, Send 하는 부분을 간단하게 구현해 볼 것이다. 😉

그래서 기존의 Session 코드에서 바뀐 부분만 살짝 보면...

 

Session.h

/*--------------
	Session
---------------*/

class Session : public IocpObject
{
public:
	void				ProcessSend(SendEvent* sendEvent, int32 numOfBytes);

public:
	// Circular Buffer [             ]
	//char _sendBuffer[1000];
	//int32 _sendLen = 0;
};

일단 ProcessSend 에서 SendEvent* 를 받을 수 있도록 인자를 추가했고, 기존에 수신 버퍼가 BYTE 타입의 배열을 타입으로 갖고 있었던 것처럼 송신 버퍼도 비슷하게 만들어 줄 수 있다.

다만 위 방식처럼 Circular Buffer 를 만들게 되면, 송신을 할 때마다 버퍼를 돌면서 채우고 빼고를 반복해 주어야 하며, 복사 비용이 많이 들어 실제로 저런 식으로 사용하지는 않는다! 😅 따라서 위는 그냥 예시로 알아두면 좋겠다.

 

Session.cpp

void Session::ProcessSend(SendEvent* sendEvent, int32 numOfBytes)
{
	sendEvent->owner = nullptr; // RELEASE_REF
	xdelete(sendEvent);

	if (numOfBytes == 0)
	{
		Disconnect(L"Send 0");
		return;
	}

	// 컨텐츠 코드에서 오버로딩
	OnSend(numOfBytes);
}

그럼 ProcessSend 의 구현을 보자. 일단 sendEvent 가 들어왔을 때 메모리를 잘 해제해 주고, 관련한 처리를 진행해 준다.

이 프로젝트에서는 게임 서버의 GameSession 클래스가 Session 클래스를 상속받아 관련 동작을 구현해 줄 것이다.

 

GameSession

class GameSession : public Session
{
public:
	virtual int32 OnRecv(BYTE* buffer, int32 len) override
	{
		// Echo
		cout << "OnRecv Len = " << len << endl;
		Send(buffer, len);
		return len;
	}

	virtual void OnSend(int32 len) override
	{ 
		cout << "OnSend Len = " << len << endl;
	}
};

일단은 간단하게 OnRecv 의 동작은 Echo 서버처럼 동작하도록 만들었다. 송신 시에는 OnSend 를 호출하는데, 로그만 찍어주도록 했다.

OnRecv 에서는 다시 데이터를 전송하는데, 이 부분은 사실 Session::Send 에 구현되어 있다.

void Session::Send(BYTE* buffer, int32 len)
{
	// 생각할 문제
	// 1) 버퍼 관리?
	// 2) sendEvent 관리? 단일? 여러개? WSASend 중첩?

	// TEMP
	SendEvent* sendEvent = xnew<SendEvent>();
	sendEvent->owner = shared_from_this(); // ADD_REF
	sendEvent->buffer.resize(len);
	::memcpy(sendEvent->buffer.data(), buffer, len);

	WRITE_LOCK;
	RegisterSend(sendEvent);
}

여기서는 사실 여러 가지 생각할 거리가 있다.

예를 들어, 버퍼를 어떻게 관리할 것인가? 라는 문제가 생길 수 있다. Send 를 여러 번 호출할 때, 결국 버퍼에 해당 정보가 쌓이는데 버퍼가 꽉 차면 어떻게 할 것인가... 부터, 버퍼를 어떤 식으로 만들 것인지(아까 간단하게 Circular Buffer 를 언급했다) 등의 문제가 있다.

또한 버퍼가 꽉 차면 여러 송신 요청들이 Pending 상태로 놓이게 되는데, 과연 Pending 에서 풀려날 때 송신이 원하던 순서대로 이루어질지에 대한 문제도 있다.

왜냐면, GetQueuedCompletionStatus 함수를 통해 잠자던 쓰레드를 깨워 Send 를 동작시킬 때, 먼저 잠든 녀석이 선순위로 깨어난다는 보장이 없기 때문이다! 😮

여기에서는 다루기 않겠지만, 그런 문제는 Page Lock 등의 기법으로 해결한다고 한다. 여백이 적어 여기에 적지는 않겠다 🤣

 
Comments