01. OT

이전에 만든 iocp서버와 언리얼은 연결하는 강의.

 

02. 복습 #1

 

알아야 할 것

- 서버 개론

- 쓰레드

하나의 프로세스에서 여러개의 코어를 활용하는 기술

쓰레드를 얼마나 사용하고 배치해는지 주의깊게 봐야한다.

cpu 부담을 가장 많이 먹는건 게임 컨텐츠 캐릭터 움직임 공격 몬스터 움직임 등. 그다음 네트워크 그다음 DB

DB처리는 거의 100퍼 블로킹. 그래서 DB를 담당 쓰레드를 따로 할당해야함. 
블로킹 논블로킹?

 

- 아토믹, 락

- 람다, 펑터

- 스마트 포인터

- 네트워크 (셀렉트, 이벤트 셀렉트, ... iocp)

- tcp vs udp

- RecvBuffer

- SendBuffer

- 패킷 조립(protobuf)

03. 복습 #2

- cpp에서 쓰레드 만들기

#include <thread>
#include <iostream>
using namespace std;

void HelloThread()
{
    while ()
		cout << "Hello Thread" << endl;
}

void main()
{
	std::thread t1(HelloThread);
    t1.join();
}

- 메모리의 스택, 힙, data 영역

쓰레드는 각자의 스택을 가지고있다(공유 x). 힙, data(글로벌 변수 등)는 공유자원이다.

- 데이터 레이스를 방지하기 위해 atomic이나 mutex을 사용한다.

- 락을 편하고 안전하게 쓰기 위해 lock_guard를 사용한다.

04. 복습 #3

- 람다, 펑터

람다의 예시 [](){} -> 이게 람다함수 아래는 func라는 이름에 람다함수를 대입연산한것.

void main()
{
    string victim = "Walker";
    int packetId = 2;
    auto func = [victim, packetId]()
    {
        //Attack victim
    }

    func();
}

 

functor의 예시

class Job
{
public:
	void Execute()
    {
    	//TODO
    }
    string victim = "Walker";
    int packetId = 2;//공격하라 패킷 ID
};

 

펑터는 객체를 만들어 상태를 저장해 준다는게 중요한 것.

일반적인 함수 포인터와 펑터를 비교하면 펑터가 상위호환이다.

함수는 변수까지 저장해둘 수는 없지만 펑터는 변수까지 저장하기 때문.

그런데 몇백개의 요청을 하나하나 펑터로 만든다면 매우 귀찮은 일이다.

그래서 람다를 사용하는 것. 펑터와 완전히 같은 역할인데 더 쓰기 쉬운 것.

 

주의할 건 아래와 같이 멤버함수 안에서 람다함수를 쓰면 문제가 발생할 수 있다.

class Job
{
public:
	auto CreateJob()
    {
    	auto func = [=]()
        {
        	cout << packetId << endl; 
        }
        return func;
    }
    string victim = "Walker";
    int packetId = 2;//공격하라 패킷 ID
};

캡쳐 [=] 로 만든 람다함수 안에서 packetId는 this->packetId 이기 때문에 Job이 만약 소멸했다면 this는 이미 유효하지 않은 포인터라서 문제가 발생한다. 안전하게 사용하려면 [packetId] 라고 써야한다.

같은 이유로 캡쳐에 포인터를 포함할 때 해당 포인터의 주소가 소멸됐다면 문제가 발생할것이니 주의하자.

 

- 메모리 풀

서버에서 메모리 풀을 없앴는데(이전 강의는 있었음) 메모리 풀의 단점은 뭘까?

메모리 풀을 쓰면 반환을 하지 않았기 때문에 메모리 침범 문제가 발생해도 크래시 터지지 않는다. 그래서 메모리 풀에서 발생한 문제는 매우 매우 파악하기가 어렵다. 그리고 요즘은 new delete의 동적할당이 빠르기 때문에 메모리 풀 자체를 잘 사용하지 않는 추세라고 한다.

 

- 스마트 포인터

진짜 완벽하게 이해하고 있어야 한다. unique, shared, weak

스마트포인터의 핵심은 레퍼런스 카운팅이다. 얘가 얼마나 사용되고 있는지 파악하고, 아무도 쓰지 않을 때 소멸시키는 것이 스마트 포인터의 가장 핵심적인 고민.

셰어드 포인터를 사용할 때의 문제는 사이클 문제인데, 그것때문에 사용하는게 귀찮고 코드가 복잡해져도 weak 포인터를 쓰거나 아니면 아예 순환되지 않도록 만들고 shared 포인터를 쓰는 방법이 있다.

쉐어드 포인터를 멀티스레드 환경에서 쓸 때 복사해서 쓰는건 괜찮지만 대입할 때 문제가 발생할 수 있다.

그래서 atomic<shared_ptr<Player>> 이렇게 아토믹에 쉐어드포인터를 넣 사용하기도 한다고 한다.

05. 복습 #4

네트워크 프로그래밍 select, iocp.

- iocp

CreateCompletionPort 함수로 핸들을 만들고

거기에 등록할 소켓(세션)을 넣어준다.

각각의 쓰레드에서 iocpcore 클래스의 Dispatch함수에서 GetQueueCompletionStatus 함수로 대기하고 있다가 네트워크 패킷이 오면 그걸 조립해서 컨텐츠까지 처리하는걸 올인원으로 하는 것.

그런데 AI는 어떻게 처리하지? AI는 클라에서 요청하는게 아니니 iocp와 관련이 없을텐데?

 

커스텀 일감(게임로직)을  잡큐에 넣어줘서 주기적으로 실행할 수 있게끔 해서 쓰레드의 일부가 그 일감을 처리하도록 만들어 주는 것.

void DoWorkerJob(ServerServiceRef& service)
{
	while (true)
	{
		LEndTickCount = ::GetTickCount64() + WORKER_TICK;

		// 네트워크 입출력 처리 -> 인게임 로직까지 (패킷 핸들러에 의해)
		service->GetIocpCore()->Dispatch(10);

		// 예약된 일감 처리
		ThreadManager::DistributeReservedJobs();

		// 글로벌 큐
		ThreadManager::DoGlobalQueueWork();
	}
}

iocpcore->dispatch(10)은 10ms만 GetQueueCompletionStatus 를 대기했다가 일이 없으면 dispatch에서 나와서 JobQueue에 예약된 일감 처리하는 것. 게임로직은 이중에서 글로벌 큐에 들어가는것이다.

 

-세션

세션은 클라의 소켓 뿐 아니라 유저데이터, db 등등을 하나에 모아둔 것. 강의자는 대사관이라고 표현함.

 

- Recv 버퍼

TCP는 순서가 지켜지지만 패킷하나를 보내도 짤려서 보내질 수 있다.

그래서 패킷헤더를 만들어서 패킷id와 사이즈를 확인한 뒤에 처리해야한다. 그래서 Recv버퍼를 사용.

당연히 session이 갖고있다.

버퍼는 커서 방식으로 관리. 세션마다 하나씩 갖고있고, 하나의 세션에 대한 Recv작업은 하나의 쓰레드가 전담하기 때문에 멀티쓰레드를 고려하지 않아도 돼서 편하다.

 

- Send 버퍼

서버 -> 클라로 보내는 상황. 이것도 session이 갖고있어야 할 것 같지만 아니다. 샌드 버퍼는 한개만 있지 않기 때문.

게임을 하면 유닛이 엄청 많을텐데 누가 공격을 하면 주변의 모든 클라에게 그 정보를 알려야하기 때문에 주변 5000명의 클라 세션에 모두 복사해서 보낸다면 큰 손실일 것이다. 가장 효과적인 방법은 하나의 Send버퍼에만 정보를 저장하고, 다른데에서는 포인터를 저장하는 방식으로 send 하는 것.

Send는 멀티쓰레드를 고려해서 만들어야한다.

 

- 패킷

패킷을 만든다는 건 파일 입출력으로 세이브 데이터 만드는것과 비슷하다.

패킷에 들어갈 데이터에 가변적인 요소가 들어갈 수가 있는데 그걸 잘 처리해야함.

패킷을 만드는 작업을 굉장히 많이 하는데 효율적으로 해야함. 직접 만들어도 되지만 protobuf가 대신 해준다.

protobuf보다 플랫버프(?) 가 더 빠르다는데 패킷을 해석할때는 오히려 힘들어서 장단이 있다.

 

언리얼에서 쓰레드를 하나 더 파서 네트워크 관련된걸 처리한다 하면, 패킷을 만들고 그거에 대한 처리를 해줘야 하는데, 네트워크 담당 쓰레드에서 Actor에 접근하면 크래시가 난다. 게임은 메인쓰레드만 담당하기 때문. 유니티도 같은 문제가 있다고 한다.

그럼  완성된 패킷의 처리는 어떻게 하는가? 언리얼에서도 잡큐를 만들어서 네트워크 담당 쓰레드가 그걸 잡큐에 넣어주고, 메인쓰레드가 그걸 꺼내서 처리하도록 만들면 된다.


 

'강의 수강 > 게임서버(2)' 카테고리의 다른 글

5. Job  (0) 2026.03.20
4. 이동 동기화  (0) 2026.03.19
3. 입장과 퇴장  (0) 2026.01.19
2. 서버 연동 기초  (0) 2026.01.10

+ Recent posts