<aside> 🧱

3. 두 모델 차이가 터진 곳이 네트워크 이야기였다면, 여기는 그 밖의 전부다.

서버는 소켓만 Windows에 묶여 있는 게 아니었다. 타이머·경로·코어 배치·종료 시그널·크래시 덤프·설정 파일·로그 시각까지, 눈에 안 띄는 곳이 더 많았다. 이 문서는 그것들을 어떻게 한곳으로 몰았는지, 그리고 몰면 안 되는 것이 하나 있었다는 이야기다.

</aside>

<aside> 📌

범위IOCP_Server/IOCP_Server/Platform/Platform.h(신설)와 전송 계층 경계. 락프리 자료구조 쪽 어댑터는 2. 락프리를 리눅스로에 따로 있다.

</aside>


1. 무엇이 Windows에 묶여 있었나

소켓을 빼고도 이만큼이었다.

갈래 Windows 리눅스
고해상도 타이머 timeBeginPeriod 대응물 없음 — std::sleep_for가 이미 충분한 해상도
실행 파일 경로 GetModuleFileNameA /proc/self/exe
코어 배치 SetThreadAffinityMask · GetProcessAffinityMask pthread_setaffinity_np · sched_getaffinity
종료 시그널 SetConsoleCtrlHandler sigaction(SIGINT·SIGTERM)
스레드 CPU 시간 GetThreadTimes (커널·유저 분리) clock_gettime(per-thread) — 커널분을 따로 못 준다
32비트 원자연산 InterlockedIncrement 계열 __atomic_*
설정 파일 GetPrivateProfileString 계열 대응물 없음 — 직접 파서를 만들었다
로그 시각 localtime_s localtime_r
크래시 덤프 미니덤프(MiniDumpWriteDump) sigaction · backtrace4장에서 따로

2. Platform — 한곳에 가두기

이 아홉 갈래를 헤더 하나(Platform.h)에 몰았다. 호출부는 Platform::SetCurrentThreadAffinity(...)처럼 부르고, 어느 OS인지 모른 채로 돈다.

// 호출부는 이렇게만 보인다
Platform::SetHighResolutionTimer(true);
Platform::InstallShutdownHandler(cb, &completeFlag, 4500);
const int cores = Platform::GetAvailableCoreCount();

<aside> 🔑

가두는 목적은 "#ifdef를 없애는 것"이 아니다. #ifdef를 한 파일에만 두는 것이다.

#ifdef가 코드 곳곳에 흩어져 있으면, 새 OS를 붙일 때 무엇을 빠뜨렸는지 알 방법이 없다. 한 파일에 모여 있으면 그 파일만 훑으면 된다 — 실제로 리눅스 팔을 채울 때 이 헤더가 할 일 목록 역할을 했다.

</aside>

두 곳은 그냥 못 옮겼다

<aside> ⚠️

① 커널 CPU 시간이 사라진다.

Windows GetThreadTimes는 커널 시간과 유저 시간을 나눠 준다. 우리는 그걸로 mmo_thread_kernel_ratio(스레드가 커널에 머문 비율)를 재고 있었다.

리눅스의 per-thread 시계는 합계만 준다. 그래서 리눅스에서는 이 지표가 0으로 고정된다. 값을 지어내는 대신 "못 재는 것"으로 남겼다 — 나중에 이 지표를 볼 때 리눅스 숫자를 IOCP와 나란히 놓으면 안 된다.

</aside>

<aside> ⚠️

② 창을 닫을 때의 정리 시간.

Windows에서 콘솔 창을 닫거나 로그오프하면, SetConsoleCtrlHandler의 핸들러가 반환하는 순간 OS가 프로세스를 죽인다. DB 최종 저장이 잘릴 수 있다.

그래서 InstallShutdownHandler에 완료 플래그와 최대 대기 시간을 받는 인자를 더했다 — 핸들러가 "정리 끝났다"는 신호를 기다렸다가 반환한다. 리눅스 sigaction 경로에는 이런 제약이 없어 인자를 받아도 그냥 통과시킨다.

</aside>

함정 — "한곳에 가둔다"가 항상 옳지는 않았다

소켓 헤더까지 Platform.h에 넣으려다 빌드가 통째로 깨졌다.

Windows 소켓 헤더에는 순서 규칙이 있다. <WinSock2.h><Windows.h>보다 먼저 와야 한다. 안 그러면 <Windows.h>가 끌고 오는 옛 winsock.h와 충돌해 sockaddr 재정의 오류가 난다.

Platform.h<WinSock2.h>를 넣자, 이 헤더를 포함하지 않는 다른 번역 단위들이 여전히 <Windows.h>를 먼저 열면서 그 충돌이 났다. WIN32_LEAN_AND_MEAN으로도 못 막았다 — 그 매크로가 없는 파일들이 문제였기 때문이다.

<aside> 🕳️

결국 소켓 헤더 책임은 IOCPServer.h로 되돌렸다. Platform은 소켓 핸들 타입(Platform::NetSocket)과 동작(CloseSocket·WouldBlock)만 가리고, 헤더 포함 순서는 원래 있던 자리가 지킨다.

교훈은 이렇다 — 격리 계층은 "무엇을 가릴지"만이 아니라 "무엇을 가리면 안 되는지"도 정해야 한다. 헤더 포함 순서처럼 번역 단위 전체에 걸리는 제약은 한곳에 몰 수 있는 종류가 아니다.

</aside>

3. 전송 경계 15개