<aside> 🔀

이 시리즈의 핵심이다. 골격은 거의 그대로 옮겨졌고, 진짜로 문제가 된 곳은 딱 두 군데였다.

둘 다 같은 원인에서 나온다 — IOCP는 "일이 끝났다"는 통지가 오는데, epoll에는 그 통지가 없다. 통지가 없다는 사실 하나가 세션의 수명송신 구조를 동시에 무너뜨렸다.

</aside>

<aside> 📌

먼저 읽으면 좋은 것 — 완료 통지와 준비 통지의 차이가 아직 낯설다면 epoll 페이지를 먼저 보면 이 문서가 훨씬 쉽게 읽힌다. 여기서는 그 차이가 실제로 무엇을 부쉈는지만 다룬다.

줄번호 인용은 전부 IOCP_Server/IOCP_Server/Transport_Epoll.cpp.

</aside>


1. 골격은 그대로였다

옮기기 전에 가장 걱정한 것은 "세션 관리를 다시 짜야 하나"였다. 결과는 아니었다.

세션 인덱스 스택 · uniqueId 발급 · 참조 카운트 · 패킷 분해 · 게임 스레드 연동 · 세션 타임아웃 — 한 줄도 옮기지 않았다. 이것들은 전부 공통 골격(IOCPServer.cpp)에 있고, OS가 갈리는 지점은 "제출하고 수거하는 방식" 하나뿐이었다.

그래서 리눅스 백엔드는 파일 하나로 끝난다 — Transport_Epoll.cpp. Windows의 Transport_Iocp.cpp·Transport_Rio.cpp와 나란히 놓이는 세 번째 팔이다.

<aside> ✅

세션 타임아웃이 이 말을 가장 잘 보여준다 — 코드를 한 줄도 안 고쳤다.

CTimingWheelstd::thread·steady_clock·sleep_for만 쓰고(TimingWheel.h), 생명주기를 건드리는 두 호출도 공통 골격에 있다 — ProcessAccept가 등록(IOCPServer.cpp:477), ProcessRecv가 갱신(:494)을 부른다. epoll 팔이 손대야 할 자리가 없었다.

확인은 대조로 했다 — 무응답 클라와 하트비트 클라를 함께 붙이고 70초를 기다렸다. 무응답만 끊기고 하트비트는 살아남았다(session_timed_out 1). 둘 다 끊겼다면 수명 갱신이 죽은 것이다 — 한쪽만 보면 몰랐을 자리다. (SESSION_TIMEOUT_SEC = 60, IOCPServer.h:535)

</aside>

2. 확장점을 새로 팔 필요가 없었다

처음 계획은 달랐다. OS마다 서버 클래스를 따로 두고(CEpollServer 같은) 그중 하나를 고르는 구조를 생각했고, 그 고르는 자리로 NetIoModel.h를 만들었다.

그런데 그 사이 전송 계층이 Transport_*.cpp로 이미 갈라졌다. RIO를 붙이면서 생긴 구조다. 그러자 클래스를 나눌 이유가 사라졌다 — 갈리는 것은 제출·수거 방식뿐이고 나머지는 공통이니, 파일 하나만 더 놓으면 됐다.

지금 NetIoModel.h에 남은 것은 이게 전부다.

using NetIoModel = CIOCPServer;   // 클래스는 하나. 갈리는 건 Transport_*.cpp

<aside> 🔑

확장점을 미리 파둔 것이 도움이 안 됐다. 정작 확장은 그쪽이 아니라 이미 뚫려 있던 다른 자리에서 일어났다.

교훈을 억지로 뽑자면 이렇다 — 확장점은 필요해질 때 만드는 편이 낫다. 미리 만든 자리는 정작 필요한 순간에 모양이 안 맞고, 그때는 이미 다른 곳에 더 자연스러운 자리가 생겨 있다.

</aside>

3. 참조를 누가 놓는가 ★

증상 — 세션이 만들어지기만 하고 안 죽었다

뼈대를 올리고 클라가 붙는 것까지 확인한 뒤, 지표를 보니 이랬다.

mmo_sessions_created   1
mmo_sessions_destroyed 0
mmo_session_count      1     ← 끊었는데도 그대로

접속을 끊어도 슬롯이 안 돌아온다. 100명이 들락날락하면 슬롯이 말라 접속을 못 받게 되는 결함이다.