1. 목적

클라이언트를 시뮬레이션한다.

엔진 코드와 서버 코드를 분리한다.

 

 

2. 프로젝트 구성

(1) 더미 클라이언트

서버를 테스트할 용도로 더미 클라이언트를 사용하기 위하여 기존의 솔루션에 DummyClient 프로젝트를 추가한다.

 

(2) 정적 라이브러리

DLL과는 다르게 컴파일 할 때 빌드 내에 포함된다.

이제 프리 컴파일 헤더(미리 컴파일된 헤더)를 다른 프로젝트에 할당해준다.

다음은 Server 프로젝트에서 pch 클래스를 만들고, Server의 pch.h의 내용을 지운 후에(pragma once 제외) 아래와 같이 설정한다.

그리고 이제 Server.cpp에 헤더를 include 해주자.

DummyClient에 대하여 위의 작업을 동일하게 적용한다.

 

마지막으로 ServerCore의 파일을 삭제해준다.

솔루션 탐색기 상태

 

3. ServerCore 구성하기

정적 라이브러리에 해당하는 ServerCore.

(1) Types.h

C++ 표준 타입을 타이핑하기 쉽게 대체하여 정의한 파일이다.

#pragma once
#include <mutex>
#include <atomic>

using int8 = __int8;
using int16 = __int16;
using int32 = __int32;
using int64 = __int64;
using uint8 = unsigned __int8;
using uint16 = unsigned __int16;
using uint32 = unsigned __int32;
using uint64 = unsigned __int64;

template<typename T>
using Atomic = std::atomic<T>;
using Mutex = std::mutex;
using CondVar = std::condition_variable;
using UniqueLock = std::unique_lock<std::mutex>;
using LockGuard = std::lock_guard<std::mutex>;

 

 

(2) CorePch.h

ServerCore에서 쓰이는 헤더 파일들을 관리하는 프리 컴파일 헤더이다.

#pragma once

#include "Types.h"
#include "CoreMacro.h"
#include "CoreTLS.h"
#include "CoreGlobal.h"

#include <vector>
#include <list>
#include <queue>
#include <stack>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>

#include <windows.h>
#include <iostream>
using namespace std;

 

(3) ServerCore -> pch.h

#pragma once

#define WIN32_LEAN_AND_MEAN // 거의 사용되지 않는 내용을 Windows 헤더에서 제외합니다.

#include "CorePch.h"

 

 

 

4. 폴더 정리 및 프로젝트 연결

(1) Binaries (Server 프로젝트)

작업한 결과물이 저장되는 폴더이다.

마지막에 \를 추가하여야 오류가 발생하지 않을 것이다.

 

 

(2) Libraries - ServerCore (ServerCore 프로젝트)

정적 라이브러리들이 저장되는 폴더이다.

 

 

(3) Intermediate

빌드 혹은 디버깅 중에 발생하는 중간 결과물이 저장되는 폴더이다.

 

 

(4) 헤더 파일 연결

Server 프로젝트에 ServerCore의 헤더 폴더를 연결한다.

나중에는 헤더 파일을 한 폴더에서 모아서 관리하게 될 것이다.

 

위의 작업들을 DummyClient에도 동일하게 적용한다.

 

(5) 정적 라이브러리 연결

Server 프로젝트에 ServerCore 라이브러리 폴더를 연결한다.

 

Server 프로젝트의 pch.h를 수정한다.

#pragma once

#include "CorePch.h"

#ifdef _DEBUG
#pragma comment(lib, "Debug\\ServerCore.lib")
#else
#pragma comment(lib, "Release\\ServerCore.lib")
#endif

이제 서로 다른 프로젝트들을 마치 하나의 프로젝트에서 작업하는 것처럼 유기적으로 사용할 수 있다.

 

위의 작업들을 DummyClient에도 동일하게 작업한다.

 

(6) 테스트

ServerCore에서 작성한 코드가 Server.cpp에서 작동하는지 확인한다.

 

 

5. 스레드를 관리하는 매니저 클래스 - ServerCore -> ThreadManager

#pragma once

#include <thread>
#include <functional>

/*------------------
	ThreadManager
-------------------*/

class ThreadManager
{
public:
	ThreadManager();
	~ThreadManager();

	void	Launch(function<void(void)> callback);
	void	Join();

private:
	static void InitTLS();
	static void DestroyTLS();

private:
	Mutex			_lock;
	vector<thread>	_threads;
};
#include "pch.h"
#include "ThreadManager.h"
#include "CoreTLS.h"
#include "CoreGlobal.h"

/*------------------
	ThreadManager
-------------------*/

ThreadManager::ThreadManager()
{
	// Main Thread
	InitTLS();
}

ThreadManager::~ThreadManager()
{
	Join();
}

void ThreadManager::Launch(function<void(void)> callback)
{
	LockGuard guard(_lock);

	_threads.push_back(thread([=]()
		{
			InitTLS();
			callback();
			DestroyTLS();
		}));
}

void ThreadManager::Join()
{
	for (thread& t : _threads)
	{
		if (t.joinable())
			t.join();
	}
	_threads.clear();
}

void ThreadManager::InitTLS()
{
	static Atomic<uint32> SThreadId = 1;
	static Atomic<uint32> LThreadId = SThreadId.fetch_add(1);
}

void ThreadManager::DestroyTLS()
{

}

 

(1) Launch 함수

락 가드로 락을 잡아준다. 이후에 쓰레드를 저장하는 _threads 벡터에 쓰레드를 집어 넣는다.

각 스레드의 Id를 TLS로 초기화하여 넣어주고 스레드에서 실행할 함수(작업)을 실행한다.

이후에 작업이 끝나면 DestroyTLS로 TLS 영역을 초기화 해주는 코드이다.

 

(2) Join 함수

모든 스레드를 대상으로 join을 발동하고, clear를 통해 벡터를 비운다.

 

(3) TLS(Thread Local Storage) 란?

추후에 글로벌 변수(또는 함수)를 사용할 때 경쟁 조건과 데드락을 방지하기 위하여 데이터를 안전하게 꺼낼 수 있는 공간.

글로벌 변수를 TLS로 선언하면 각 스레드 별로 독립적인 공간에서 그것을 들고 있게 된다.

(각 스레드가 가지는 별도의 공간이다)

 

- CoreTLS

#pragma once

extern thread_local uint32 LThreadId;
#include "pch.h"
#include "CoreTLS.h"

thread_local uint32 LThreadId = 0;

thread_local 타입을 이용하여 TLS 공간에 LThreadId (스레드 별 고유 ID 번호)를 저장한다.

 

(4) InitTLS

이제 스레드 매니저가 생성되면 메인 스레드를 1번으로 고정하고, 생성되는 스레드 별로 번호가 1씩 증가하는 LThreadId 값을 가지게 된다.

 

 

6. ThreadManager를 외부에서 사용하기

이제 ThreadManager를 외부에서 사용한다. 기존의 싱글톤 방식과 유사하지만 여기에서는 스마트 포인터를 이용하여 사용할 것이다.

(1) ServerCore -> CoreGlobal

#pragma once

class ThreadManager;

extern std::unique_ptr<ThreadManager> GThreadManager;
#include "pch.h"
#include "CoreGlobal.h"
#include "ThreadManager.h"

unique_ptr<ThreadManager> GThreadManager = make_unique<ThreadManager>();

 

 

(2) Server-> Server.cpp

이제 스레드 매니저를 외부에서 실험할 차례이다.

#include "ThreadManager.h"

using namespace std;

void TestThread()
{
	cout << LThreadId << "번 스레드" << endl;

	while (true)
	{

	}
}


int main()
{
	for (int32 i = 0; i < 10; i++)
		GThreadManager->Launch(TestThread);

	GThreadManager->Join();
}

스레드를 10개 만들어서 스레드의 번호가 TLS에서 정상적으로 부여되는지 확인한다.

 

오류가 발생한다면 중간 생성 폴더를 정리해주고 다시 빌드한다.

 

 

7. 디버그 시에 서버와 클라이언트를 동시에 테스트하기

솔루션을 선택하여 우클릭 -> 속성에서 위와 같이 설하면 두 프로젝트가 함께 실행될 것이다.

 

단, 디버그를 실행하면 출력에 동시에 되는 것은 스레드 세이프(Thread-Safe)가 확보되지 않았기 때문을 염두에 둔다.