인프런 커뮤니티 질문&답변

100end님의 프로필 이미지
100end

작성한 질문수

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동

클라이언트 강제 종료 시 플레이어 삭제 기능

작성

·

54

·

수정됨

0

안녕하세요 루키스님
part4 강의로 만들어진 서버와
part5에서 만들어진 클라이언트를
조금 수정해가며 추가 기능을 만들던 중
문제가 생겼는데 해결이 잘 안되어서 질문드립니다.


우선 추가하려는 기능은 강의에서 배운 Q를 눌러서 종료하는 기능 외에
플레이어(3명)들이 접속해있는 상태에서 언리얼 편집창을 닫아버렸을 때
3명 모두 접속이 끊어지며 플레이어들을 서버에서 제거해주는 기능을 추가하려고 합니다..

이 때, 발생하는 문제는
player가 3명이고 objectId가 각각 1번부터 3번까지 있다고 했을 때,
어쩔 땐 player들 모두 제거되고,
어쩔 땐 player(2번), player(1번), player(1번)처럼
objectId가 1번인 플레이어를 제거하는 처리가 중복 호출되며
2번과 1번에 대한 삭제만 이루어지고,
3번에 대한 삭제가 이루어지지 않는 문제가 발생합니다.


아래는 작성한 코드입니다.
먼저 GameSession::OnDisconnected()의 맨 마지막 줄에 GRoom->DoAsync()로
Room::HandleLeavePlayer를 호출해주는 코드를 추가했고,

이 때 들어오는 player들의 objectId들을 확인해봤을 때
1, 2, 3 모두 중복 없이 들어오는 것을 확인했습니다.

void GameSession::OnDisconnected()
{
	if (player.load() == nullptr)
	{
		return;
	}
	cout << "objectId: " << player.load().get()->objectInfo->object_id() << endl;

	RoomRef room = player.load()->room.load().lock();
	if (room == nullptr)
	{
		return;
	}

	GRoom->DoAsync(&Room::HandleLeavePlayer, player.load());
}

다음은 Room::LeaveRoom()에서 위에서 말한 상황일 때
파라미터로 들어오는 object가 중복으로 들어오는 걸 확인했습니다.
if(!success) { return success; }는
이미 삭제된 object를 중복으로 RemoveObject(objectId)를 할 경우 false를 돌려주는데
밑에서 패킷 보내는 작업을 넘겨버리기 위해 추가했습니다.
그리고 코드 거의 마지막 부분에
GSessionManager.Remove(session)로

세션을 제거하는 코드를 추가했습니다.

bool Room::LeaveRoom(ObjectRef object)
{
	if (object == nullptr)
	{
		return false;
	}

	const uint64 objectId = object->objectInfo->object_id();
	bool success = RemoveObject(objectId);
	if (!success)
	{
		return success;
	}

	// 퇴장할 플레이어에게 퇴장 패킷 전송
	if (object->IsPlayer())
	{
		PlayerRef player = static_pointer_cast<Player>(object);

		Protocol::S_LEAVE_GAME leavePkt;

		SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(leavePkt);
		if (GameSessionRef session = player->session.lock())
		{
			session->Send(sendBuffer);
		}
	}

	// 남아있는 플레이어들에게 퇴장 플레이어의 디스폰 패킷 전송
	{
		Protocol::S_DESPAWN despawnPkt;
		despawnPkt.add_object_ids(objectId);

		SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(despawnPkt);
		Broadcast(sendBuffer, objectId);

		// 퇴장 플레이어는 이미 room에서 빠져나간 상태라 Broadcast 불가
		if (object->IsPlayer())
		{
			PlayerRef player = static_pointer_cast<Player>(object);
			if (GameSessionRef session = player->session.lock())
			{
				session->Send(sendBuffer);
				GSessionManager.Remove(session);
			}
		}
	}

	return success;
}


GameSession 소멸자

virtual ~GameSession() override
{
	cout << "~GameSession" << endl;
	player.store(shared_ptr<Player>());
}

Player 소멸자

Player::~Player()
{
	cout << "~Player" << endl;
	session.reset();
}

다음은 첫번째 실행 화면입니다.
(실행 화면에 출력되는 줄이 뒤죽박죽이라 서로 다른 색으로 구별 해봤습니다..ㅠ)
연두색으로 묶은 범위는 언리얼 에디터를 1번 실행했다가 닫았을 때의 단위로 묶었습니다.
빨간색으로 밑줄 그어진 objectId: 숫자로 출력되는 부분은
GameSession::OnDisconnected()에서 출력하고 있고,
노란색으로 밑줄 그어진 Remove Objedct숫자로 출력되는 부분은
Room::RemoveObject()에서 objectId를 출력하고 있습니다.

게임서버 캡처.png
첫번째 실행 화면에서는 처음 실행하고 바로 해당 문제가 발생했습니다.
objectId: 3, objectId: 2, objectId: 1 순으로 중복 없이 잘 들어왔는데
Remove Object3, Remove Object1, Remove Object1 처럼 출력되었고,
2번 플레이어에 대한 제거가 되지 않았습니다.
때문에 두번째, 세번째 실행에서는 접속한 3명의 플레이어가 모두 잘 제거됨에도
처음에 제거되지 않은 플레이어 하나가 남아서
언리얼 에디터 실행 화면에 보이는 플레이어가 총 4명이었습니다.


다음은 두번째 출력 화면입니다.
게임서버 캡처2.png
두번째 실행 화면에서는 5번째가 되어서야 해당 문제가 발생했습니다.
그 전까지는 문제 없이 3명의 플레이어 모두 잘 제거되었습니다.
마찬가지로 objectId: 13, objectId: 14, objectId: 15로 들어왔고
Remove Object13, Remove Object15, Remove Object15로 실행되면서
13, 15번의 플레이어만 제거되고 14번 플레이어는 제거되지 않았습니다.


실제로 문제가 발생할 경우의 호출 스택을 따라가 봤을 때 OnDisconnected()에서
GRoom->DoAsync(&Room::HandleLeavePlayer, player.load())로
삭제 작업 예약 거는 것 까지는 플레이어가 중복 없이 잘 들어가는데

예약했던 Room::HandleLeavePlayer()가 호출되고
파라미터로 들어오는 플레이어의 objectId를 확인해보면
이미 삭제된 플레이어가 한번 더 들어오는 경우가 생기네요.

혹시 어떤 부분 때문일까요??

답변 1

0

안녕하세요, 인프런 AI 인턴입니다. 100end님.

비정상 연결 종료 시 플레이어 제거에 관련한 문제를 해결하기 위해, 아래의 내용을 확인하고 수정하는 것을 권장드립니다.

문제 원인

현재 문제는 여러 스레드가 동시에 플레이어 제거 작업을 수행하다 보니, 중복된 제거 시도가 발생하면서 클라이언트가 예상대로 제거되지 않는 상황입니다. 이를 해결하기 위해선 Thread-safe한 방어 기법을 사용할 필요가 있습니다.

기존 유사한 질문 및 답변

  1. [마크다운으로 링크와 제목 구현]

    • 제목 : OnDisconnect에 관한 질문입니다.
    • 요약 : OnDisconnect 메소드에서의 안전한 session 삭제 관련 문제를 다루고 있습니다. C# 특성상 참조관리를 통해 메모리를 자동으로 관리합니다. 누군가 참조하고 있다면 해당 메모리를 완전 삭제하지 않기 때문에 별다른 문제가 없다는 설명입니다.
  2. [마크다운으로 링크와 제목 구현]

    • 제목 : Room에서 _objects 자료구조는 안전한가요?
    • 요약 : _objects 자료구조는 thread-safe 하지 않으므로, 동시에 접근할 때 안전하게 관리되도록 코드를 수정해야 합니다. 본 강의에서도 동시성 이슈를 해결하는 방법을 설명하고 있습니다.

해결 방안

위 문제를 해결하는 데 있어 몇 가지 중요한 사항을 체크하고 수정합니다.

1. DoAsync 호출

현재 GameSession::OnDisconnected 메소드에서 DoAsync를 통해 비동기 작업을 예약하고 있습니다. 그러나 중복된 호출이 발생하지 않도록 보장할 필요가 있습니다.

2. Thread-safe한 자료구조 사용

플레이어 삭제 작업이 여러 스레드에서 일어날 때 동기화 문제를 해결하기 위해 Mutex 또는 다른 동기화 기법을 사용할 수 있습니다.

아래 예제는 Mutex를 사용한 동기화를 활용합니다.

void GameSession::OnDisconnected()
{
    if (player.load() == nullptr)
    {
        return;
    }
    cout << "objectId: " << player.load().get()->objectInfo->object_id() << endl;

    RoomRef room = player.load()->room.load().lock();
    if (room == nullptr)
    {
        return;
    }

    std::unique_lock<std::mutex> lock(_mutex); // 동기화
    if (_isDisconnected)
    {
        return;
    }

    _isDisconnected = true;
    GRoom->DoAsync(&Room::HandleLeavePlayer, player.load());
}

향후 참고 사항

다른 질문에서도 비슷한 문제에 대한 논의가 이루어졌으므로, 관련 내용을 참고하여 보다 안정적인 코드를 작성하는 데 도움이 될 것입니다:
- C# 특성을 이용한 참조관리 (OnDisconnect 관련 내용)
- 비동기 작업의 thread-safe 처리

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

100end님의 프로필 이미지
100end
질문자

아... 전에 잘못 넣은 코드 때문이었네요...
드디어 해결했습니다 ㅜ

100end님의 프로필 이미지
100end

작성한 질문수

질문하기