Listener의 책임과 경계
한 줄 정의
Listener는 "접속 수락 파이프라인(AcceptEx)을 유지"하는 서버 전용 IocpObject입니다.
책임/비책임
| 구분 | Listener가 한다 | Listener가 하지 않는다 |
|---|
| 네트워크 | listen 소켓 관리, AcceptEx 선등록/재등록 | 게임 규칙 처리 |
| 세션 | 접속 완료된 소켓을 Session으로 연결 | 플레이어 상태 갱신 |
| 운영 | accept 파이프라인 깊이 유지 | DB 호출/비즈니스 로직 |
구조
class Listener : public IocpObject
{
public:
HANDLE GetHandle() override;
void Dispatch(IocpEvent* event, int32 numOfBytes) override;
bool StartAccept(const ServerServiceRef& service);
};
StartAccept 부트스트랩
초기화 순서
| 순서 | 작업 | 핵심 포인트 |
|---|
| 1 | listen 소켓 생성 | Overlapped 소켓으로 생성 |
| 2 | 소켓 옵션 설정 | 재시작 정책(SO_REUSEADDR/SO_EXCLUSIVEADDRUSE) |
| 3 | Bind + Listen | 접속 수신 가능 상태 진입 |
| 4 | IOCP 등록 | Listener를 Completion Key로 연결 |
| 5 | AcceptEx 선등록 | acceptCount만큼 미리 낚싯대 던짐 |
예시 코드
bool Listener::StartAccept(const ServerServiceRef& service)
{
_service = service;
_listenSocket = SocketUtils::CreateSocket();
if (_listenSocket == INVALID_SOCKET)
return false;
SocketUtils::SetReuseAddress(_listenSocket, true);
SocketUtils::Bind(_listenSocket, _service->GetNetAddress());
SocketUtils::Listen(_listenSocket);
if (!GIocpCore.Register(shared_from_this()))
return false;
for (int32 i = 0; i < _acceptCount; ++i)
{
AcceptEvent* ev = PopAcceptEventFromPool();
ev->Init();
ev->owner = shared_from_this();
if (!RegisterAccept(ev))
return false;
}
return true;
}
acceptCount의 의미
- 선등록 수가 너무 작으면 접속 피크에서 지연/드랍이 늘어납니다.
- 너무 크면 이벤트/세션 임시 자원 사용량이 증가합니다.
- 시작값은 워커 수의 2~4배 수준으로 두고 지표로 조정하는 방식이 실무에서 안전합니다.
RegisterAccept (AcceptEx 등록 단계)
AcceptEx 핵심 인자
| 인자 | 의미 |
|---|
| listen socket | 접속을 받는 소켓 |
| accept socket | 미리 생성한 클라이언트 소켓 |
| output buffer | 초기 수신 데이터 + local/remote 주소 저장 버퍼 |
OVERLAPPED* | 완료 시 되돌아올 Accept 이벤트 컨텍스트 |
주소 버퍼 크기 공식은 보통 (주소구조체 크기 + 16) * 2 + 초기수신크기를 사용합니다.
반환값 해석
| 결과 | 해석 | 대응 |
|---|
TRUE | 즉시 완료 | 곧바로 완료 처리 가능 |
FALSE + WSA_IO_PENDING | 정상 대기 | 완료 통지 기다림 |
| 그 외 오류 | 등록 실패 | 소켓 정리 후 재시도/로그 |
예시 코드
bool Listener::RegisterAccept(AcceptEvent* ev)
{
ev->Init();
ev->session = _service->CreateSession();
DWORD bytes = 0;
BOOL ok = SocketUtils::AcceptEx(
_listenSocket,
ev->session->GetSocket(),
ev->addressBuffer,
0,
kAddressBytes, kAddressBytes,
&bytes,
&ev->overlapped);
if (ok == TRUE)
return true;
int32 err = WSAGetLastError();
return (err == WSA_IO_PENDING);
}
ProcessAccept (완료 처리 단계)
호출 경로
GQCS -> Listener::Dispatch -> ProcessAccept
처리 순서
| 순서 | 작업 | 이유 |
|---|
| 1 | SO_UPDATE_ACCEPT_CONTEXT 적용 | accept 소켓을 listen 컨텍스트와 연결 |
| 2 | 원격 주소 추출 | 세션 주소 정보 설정 |
| 3 | Session을 IOCP에 등록 | 이후 Recv/Send 완료 수신 준비 |
| 4 | 서비스에 세션 추가 | 세션 라이프사이클 관리 시작 |
| 5 | ProcessConnect/RegisterRecv | 수신 파이프라인 시작 |
| 6 | RegisterAccept 재호출 | 다음 접속을 위한 파이프라인 유지 |
실패 경로 규칙
- 중간 단계 실패 시 해당 accept socket/session은 정리합니다.
- 성공/실패와 무관하게 마지막에 Accept 재등록을 보장해야 파이프라인이 끊기지 않습니다.
- 이 재등록을
finally 성격 코드로 고정하면 실수를 크게 줄일 수 있습니다.
주소 처리와 NetAddress 연동
주소 추출 방식
| 방식 | 장점 | 주의 |
|---|
GetAcceptExSockaddrs | AcceptEx 버퍼에서 바로 추출 가능 | 버퍼 크기/오프셋 규칙 정확히 필요 |
getpeername | 코드가 직관적 | SO_UPDATE_ACCEPT_CONTEXT 적용 후 사용 권장 |
NetAddress 반영
NetAddress addr = SocketUtils::GetRemoteAddress(ev->addressBuffer);
session->SetNetAddress(addr);
운영 관점
- 접속 로그에
sessionId, ip, port, acceptLatency를 남기면 장애 분석이 쉬워집니다.
AcceptEx vs 일반 accept (엔진 관점)
| 항목 | 일반 accept | AcceptEx |
|---|
| 모델 | 동기/논블로킹 기반 루프 | IOCP 완료 기반 비동기 |
| 소켓 생성 | accept가 반환 | 미리 생성해 전달 |
| 완료 통지 | 직접 호출 결과 확인 | Completion Port에서 수신 |
| 초기 데이터/주소 | 별도 API 호출 | output buffer로 함께 처리 가능 |
| 대규모 서버 적합성 | 제한적 | 높음(IOCP 파이프라인과 궁합) |
운영 튜닝 포인트
선등록 깊이 관리
acceptCount가 낮으면 접속 피크에서 큐 대기 증가
acceptCount가 높으면 메모리/이벤트 풀 사용량 증가
관측 지표
- 초당 접속 수(accept completions/sec)
- accept 등록 실패율
- 접속 지연(accept 완료 -> session connect 완료)
- 선등록 이벤트 잔량(현재 pending accept 수)
자주 발생하는 장애 패턴
| 패턴 | 증상 | 원인 |
|---|
| 재등록 누락 | 일정 시점부터 신규 접속 정지 | ProcessAccept 후 재등록 빠짐 |
| 컨텍스트 미적용 | getpeername 실패/주소 이상 | SO_UPDATE_ACCEPT_CONTEXT 누락 |
| 풀 고갈 | 접속 폭주 시 실패율 상승 | 이벤트/세션 풀 크기 부족 |
강의 시 유의사항
강조 포인트
- Listener는 "접속 파이프라인 관리자"이지 "게임 로직 실행기"가 아닙니다.
RegisterAccept -> ProcessAccept -> RegisterAccept 순환이 핵심입니다.
- 재등록 누락은 실습에서 가장 먼저 확인해야 할 체크포인트입니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| 접속 완료 후 등록은 끝났다 | Accept 파이프라인은 항상 일정 깊이로 유지해야 한다 |
WSA_IO_PENDING은 오류다 | 정상 비동기 대기 상태다 |
| Address 추출은 아무 때나 가능하다 | 컨텍스트 업데이트/버퍼 규칙을 지켜야 안전하다 |
체크 질문 (스스로 답해보기)
- 왜 Listener는 성공/실패와 무관하게 Accept를 재등록해야 하는가?
acceptCount를 늘릴지 줄일지 어떤 지표로 판단할 것인가?
- AcceptEx 완료 후 Session 수명과 이벤트 수명을 어떻게 연결해 안전하게 관리할 것인가?