C++ 서버 구조 핵심 정리

서버의 본질은 결국 요청을 받고, 처리하고, 응답하는 것이다.

Client -> Connection -> Receive -> Packet -> Game Logic -> Send -> Client

여기에 여러 클라이언트가 동시에 접속하면서 I/O를 효율적으로 처리해야 하는 문제가 생긴다.

Client -> TCP -> Socket -> I/O Model -> Session -> Buffer -> Packet -> Game Logic -> Send

TCP 서버 기본 흐름

Socket -> Bind -> Listen -> Accept -> Client Socket -> Recv / Send

Listen Socket은 연결을 받는 용도이고, 실제 통신은 Client Socket을 통해 이루어진다.

Server -> Listen Socket -> Client Connection -> Client Socket

TCP와 Packet 처리

TCP는 Packet 단위가 아니라 Byte Stream으로 데이터를 전달한다.

Client Packet -> TCP Stream -> Receive Buffer -> Packet Framing -> Packet -> Handler

하나의 Send가 하나의 Receive로 대응된다는 보장은 없다.

Send Packet A -> TCP Stream -> Receive A 일부 -> Receive A 나머지

여러 Packet이 하나의 Receive에 합쳐질 수도 있다.

Send A -> Send B -> TCP Stream -> Receive A + B

따라서 일반적인 Packet 처리 흐름은 다음과 같다.

Receive -> Buffer 추가 -> Header 확인 -> Packet Length 확인 -> 데이터 충분 여부 확인 -> Packet 추출 -> Handler

Blocking Socket

Blocking Socket에서는 I/O 작업이 완료될 때까지 현재 Thread가 기다린다.

recv() -> 데이터 대기 -> 데이터 도착 -> 처리

연결마다 Thread를 사용하는 구조라면 다음처럼 된다.

Client A -> Thread A
Client B -> Thread B
Client C -> Thread C
Client N -> Thread N

연결 수가 증가하면 Thread 관리 비용과 Context Switching 비용이 증가할 수 있다.


IOCP

Windows에서는 IOCP를 사용해 비동기 I/O를 처리할 수 있다.

핵심 흐름은 다음이다.

WSARecv() -> I/O 요청 -> Windows -> I/O 처리 -> I/O 완료 -> IOCP -> GetQueuedCompletionStatus() -> Worker Thread -> Packet 처리

IOCP의 핵심은 Completion이다.

I/O 요청 -> I/O 완료 -> Completion Queue -> Completion 처리

IOCP 자체가 Thread Pool은 아니다.

IOCP -> 완료된 I/O 전달 -> Worker Thread -> 실제 작업 처리

IOCP Overlapped I/O

비동기 I/O 작업의 상태를 OVERLAPPED를 통해 관리한다.

WSARecv() -> OVERLAPPED -> Windows I/O -> I/O 완료 -> IOCP -> OVERLAPPED 확인 -> Session 확인 -> IO Type 확인 -> 처리

실제 서버에서는 I/O 작업 정보를 확장해서 관리하는 경우가 많다.

IO Context -> OVERLAPPED + Session + Buffer + IO Type

I/O 종류는 다음처럼 나눌 수 있다.

Accept -> Recv -> Send

IOCP Worker 구조

Worker Thread 여러 개가 하나의 IOCP에서 완료된 작업을 가져갈 수 있다.

I/O Request -> Windows -> IOCP -> Worker Thread -> Packet Processing

여러 Worker를 사용한다면:

IOCP -> Worker 1
IOCP -> Worker 2
IOCP -> Worker 3

핵심 관계는 다음이다.

IOCP -> Completion 전달
Worker Thread -> Completion 처리

Linux epoll

Linux에서는 epoll을 사용할 수 있다.

핵심 흐름은 IOCP와 다르다.

Socket -> epoll_ctl() -> Socket 감시 -> I/O 가능 상태 -> epoll_wait() -> Socket 확인 -> recv() / send()

epoll의 핵심은 Readiness다.

Socket 등록 -> I/O 가능 상태 감지 -> Application 통지 -> Application이 I/O 수행

IOCP와 달리 epoll이 데이터를 대신 읽어주는 것은 아니다.

epoll_wait() -> Read 가능 확인 -> recv() -> 데이터 처리

epoll Event Loop

일반적인 epoll 서버는 Event Loop 구조와 함께 사용된다.

Socket 등록 -> epoll_wait() -> Event 확인 -> Socket 처리 -> recv() / send() -> Event Loop 반복

여러 Socket을 하나의 Event Loop에서 감시할 수 있다.

Socket A -> epoll
Socket B -> epoll
Socket C -> epoll
Socket D -> epoll

실제 실행 흐름은 다음과 같다.

epoll -> 여러 Socket 감시 -> I/O 가능 Socket 확인 -> 해당 Socket 처리 -> 다음 이벤트 대기

Non-blocking Socket

epoll에서는 일반적으로 Non-blocking Socket을 사용한다.

epoll_wait() -> READ 가능 -> recv() -> recv() -> recv() -> EAGAIN -> 현재 처리 종료

특히 Edge Trigger 방식에서는 가능한 데이터를 모두 읽는 구조가 중요하다.

epoll_wait() -> recv() 반복 -> EAGAIN -> Event 처리 종료

Level Trigger

Level Trigger에서는 조건이 계속 유지되면 이벤트가 다시 발생할 수 있다.

데이터 도착 -> EPOLLIN -> 일부 데이터 읽음 -> 데이터 잔여 -> EPOLLIN -> 남은 데이터 처리

Edge Trigger

Edge Trigger에서는 상태 변화가 발생한 시점을 기준으로 이벤트를 받는다.

데이터 없음 -> 데이터 도착 -> EPOLLIN -> recv() 반복 -> EAGAIN

따라서 ET에서는 일반적으로 다음 흐름이 된다.

epoll_wait() -> EPOLLIN -> recv() 반복 -> EAGAIN -> 다음 Event

IOCP와 epoll

두 기술의 핵심 차이는 다음 하나로 잡으면 된다.

IOCP -> Completion
epoll -> Readiness

좀 더 풀면 다음과 같다.

IOCP -> I/O 요청 -> OS 처리 -> I/O 완료 -> Completion 전달 -> Application 처리
epoll -> Socket 감시 -> I/O 가능 -> Application 통지 -> Application이 I/O 수행

따라서 전체 흐름을 비교하면:

IOCP -> Async I/O Request -> OS -> Completion -> Worker -> Processing
epoll -> Socket Monitoring -> Readiness -> epoll_wait() -> recv / send -> Processing

I/O 모델과 실행 구조

여기서 중요한 구분이 있다.

IOCP와 epoll은 OS의 I/O 모델이고, Event Loop와 Worker Pool은 Application의 실행 구조다.

IOCP / epoll -> OS I/O Model
Event Loop / Worker Pool / Game Thread -> Application Execution Model

따라서 다음처럼 고정해서 생각하면 안 된다.

IOCP -> Worker Pool
epoll -> Event Loop

실제로는 다음처럼 조합할 수 있다.

IOCP -> Worker Thread
IOCP -> Worker Pool
epoll -> Event Loop
epoll -> Event Dispatch -> Worker Pool

Session 구조

Socket을 그대로 사용하는 것보다 Session이라는 객체로 연결을 추상화하는 경우가 많다.

Socket -> Session

Session 안에는 연결에 필요한 상태를 관리한다.

Session -> Socket + Receive Buffer + Send Buffer + Connection State + User State

전체 흐름은 다음과 같다.

Socket -> Session -> Receive Buffer -> Packet -> Handler -> Game Logic

Receive 처리

게임 서버의 실제 Receive 흐름은 다음과 같이 볼 수 있다.

Client -> TCP -> Socket -> I/O Model -> Session -> Receive Buffer -> Packet Framing -> Packet -> Packet Handler -> Game Logic

Packet Framing 과정은 다음과 같다.

Receive -> Buffer 추가 -> Header 확인 -> Length 확인 -> Packet 완성 -> Packet 추출 -> Handler

Send 처리

Send 역시 비동기로 관리하는 경우가 많다.

Game Logic -> Packet 생성 -> Send Queue -> Send Buffer -> Async Send -> I/O 완료 -> 다음 Send

여러 곳에서 하나의 Session에 Send를 요청할 수도 있다.

Game Logic A -> Send Queue
Game Logic B -> Send Queue
Game Logic C -> Send Queue

이후 하나의 흐름으로 처리한다.

Send Queue -> Send Buffer -> Async Send -> Completion / Writable Event -> 다음 Send

멀티스레딩

Network Thread와 Game Thread를 분리하면 다음과 같은 구조를 만들 수 있다.

Network I/O -> Packet -> Packet Queue -> Game Thread -> Game State

여러 Network Worker가 존재할 수도 있다.

IOCP / epoll -> Network Worker -> Packet Queue -> Game Thread -> Game State

이 구조에서는 Network Layer가 게임 상태를 직접 변경하지 않고 Packet을 Game Thread에 전달하도록 만들 수 있다.


동시성

여러 Thread가 같은 데이터를 접근하면 동시성 문제가 발생한다.

Worker A -> Shared Data
Worker B -> Shared Data

이 과정에서 Race Condition이 발생할 수 있다.

Thread A -> Read / Write
Thread B -> Read / Write
        -> Race Condition

이를 해결하기 위해 상황에 따라 동기화 구조를 사용한다.

Shared Data -> Mutex / Atomic / Lock-free Queue / Thread Ownership

하지만 더 중요한 방법은 공유 자체를 줄이는 것이다.

Network Thread -> Packet Queue -> Game Thread -> Game State

libuv

플랫폼별 I/O API를 직접 사용하는 대신 libuv 같은 추상화 계층을 사용할 수 있다.

Application -> libuv -> Platform I/O

운영체제에 따라 내부적으로 다른 I/O 기술을 사용한다.

Application -> libuv -> Windows -> IOCP
Application -> libuv -> Linux -> epoll

따라서 개발자가 플랫폼별 I/O 구현을 직접 분리해야 하는 부담이 줄어든다.


libuv Event Loop

libuv에서는 Event Loop가 핵심 실행 구조다.

Application -> libuv -> Event Loop -> Network Event -> Callback -> Packet 처리

TCP 서버를 기준으로 보면:

Client -> Network -> libuv -> Event Loop -> Read Callback -> Packet 처리

전체 서버 구조

지금까지의 내용을 하나로 묶으면 다음과 같다.

Client -> TCP -> Socket -> IOCP / epoll -> Session -> Receive Buffer -> Packet Framing -> Packet -> Packet Handler -> Game Logic -> Game State

Send까지 포함하면:

Client -> TCP -> Socket -> IOCP / epoll -> Session -> Receive Buffer -> Packet Framing -> Packet -> Handler -> Game Logic -> Send Queue -> Send Buffer -> IOCP / epoll -> TCP -> Client

멀티스레딩까지 포함하면:

Client -> TCP -> Socket -> IOCP / epoll -> Network Worker -> Packet Queue -> Game Thread -> Game Logic -> Game State

C++ 서버 전체 계층

서버를 계층으로 나누면 다음처럼 볼 수 있다.

Client -> Network -> Socket -> I/O Model -> Session -> Buffer -> Packet -> Handler -> Game Logic -> Game State -> Database

각 계층의 역할은 분리된다.

Socket -> Network Connection
I/O Model -> Efficient I/O
Session -> Connection State
Buffer -> Stream Data Management
Packet -> Protocol Data
Handler -> Request Routing
Game Logic -> Business Rules
Game State -> Runtime State
Database -> Persistent Data

IOCP

WSARecv() -> Windows -> Async I/O -> I/O Completion -> IOCP -> GetQueuedCompletionStatus() -> Worker -> Session -> Packet

epoll

Socket -> epoll_ctl() -> epoll -> epoll_wait() -> Readiness Event -> recv() / send() -> Session -> Packet

libuv

Application -> libuv -> Event Loop -> Platform I/O -> Callback -> Packet

핵심 흐름

결국 이 글에서 가장 중요한 흐름은 이것이다.

Client -> TCP -> Socket -> I/O Model -> Session -> Buffer -> Packet -> Handler -> Game Logic -> Game State

그리고 운영체제별 I/O 처리만 분리하면:

Windows -> IOCP -> Completion
Linux -> epoll -> Readiness
Cross Platform -> libuv -> Event Loop -> Platform I/O

서버의 실행 구조까지 포함하면:

Network I/O -> Packet -> Queue -> Game Thread -> Game State

IOCP와 epoll은 서버 그 자체가 아니다.

IOCP / epoll -> I/O 처리
Session -> 연결 관리
Buffer -> Stream 관리
Packet -> Protocol 처리
Queue -> Thread 간 작업 전달
Game Logic -> 실제 게임 처리
Game State -> 게임 상태 관리

결국 서버 개발에서 봐야 할 핵심 흐름은 다음 하나로 압축된다.

Client -> Network I/O -> Session -> Buffer -> Packet -> Queue -> Game Logic -> Game State -> Response -> Client

이 구조를 잡아두면 IOCP, epoll, libuv가 서로 다른 기술임에도 서버 전체에서 정확히 어느 위치에 존재하는지가 명확해진다.