Register vs Process: 구현의 뼈대
두 단계의 역할
| 구분 | Register | Process |
|---|
| 의미 | 비동기 I/O 요청 등록 | 완료된 I/O 결과 처리 |
| 시점 | "지금 일 시켜둠" | "나중에 완료 알림 받음" |
| 실행 주체 | 호출한 스레드 | IOCP 워커 스레드 |
쌍으로 동작
RegisterConnect <-> ProcessConnect
RegisterRecv <-> ProcessRecv
RegisterSend <-> ProcessSend
RegisterDisconnect <-> ProcessDisconnect
핵심 규칙
- Register 없이 Process는 오지 않습니다.
- Process가 끝난 뒤 다음 Register를 누락하면 파이프라인이 멈춥니다.
Recv와 Send의 본질적 차이
비교
| 항목 | Recv | Send |
|---|
| 트리거 주체 | 상대가 보낼 때 | 내가 보내고 싶을 때 |
| 일반 패턴 | 세션당 1개의 outstanding Recv | 송신 큐 기반 다건 송신 |
| 재등록 | ProcessRecv 끝에서 필수 | 큐 잔량/전송 상태에 따라 등록 |
| 동기화 | 상대적으로 단순 | 멀티스레드 경쟁 관리 필수 |
Recv 1개 정책이 흔한 이유
- 완료 순서/버퍼 관리가 단순해집니다.
- 디버깅이 쉬워지고 레이스 가능성이 줄어듭니다.
- 대부분의 게임 서버에서 처리량 대비 안정성이 좋습니다.
Send는 큐 중심으로 설계
- 여러 스레드가 동시에
Send를 호출할 수 있으므로 세션별 송신 큐가 필요합니다.
- "전송 중 플래그"로 중복
RegisterSend를 막는 패턴이 실무 표준입니다.
Session 콜백 계약(Contract)
콜백 포인트
| 함수 | 의미 |
|---|
OnConnected | 연결 성립 알림 |
OnRecv | 수신 데이터 처리 요청 |
OnSend | 송신 완료 알림 |
OnDisconnected | 연결 종료 알림 |
OnRecv 반환 규칙
- 반환값은 "소비한 바이트 수"입니다.
- 반드시
0 <= processedLen <= dataSize를 만족해야 합니다.
- 이 규칙이 깨지면 ReceiveBuffer 커서가 망가져 패킷 파싱이 무너집니다.
스레드 경계
- 콜백은 보통 워커 스레드에서 호출됩니다.
- 무거운 게임 로직은 JobQueue로 넘겨 워커 점유 시간을 짧게 유지해야 합니다.
ProcessRecv 구현 핵심
표준 순서
1) OnWrite(numBytes) // 수신된 바이트 반영
2) dataSize 계산
3) OnRecv(readPtr, dataSize) // 컨텐츠가 소비 길이 반환
4) OnRead(processedLen) // 소비 반영
5) Clean() // 커서 정리/압축
6) RegisterRecv() // 다음 수신 재등록
종료/오류 분기
numBytes == 0이면 원격 종료로 보고 Disconnect 경로로 전환합니다.
- 실패 완료(
GQCS FALSE)도 정리 경로로 안전하게 흘려야 합니다.
안전 검증 코드
int32 processed = OnRecv(buffer, dataSize);
if (processed < 0 || processed > dataSize)
{
Disconnect(L"invalid processed length");
return;
}
ProcessSend 구현 핵심
흔한 패턴
- 송신 완료 바이트만큼 큐/버퍼에서 소모
- 잔여 데이터가 있으면 다음
RegisterSend
- 잔여가 없으면 "전송 중 플래그" 해제
부분 전송 대응
- 한 번의 Send 완료가 요청한 전체 바이트를 보장하지 않습니다.
- 잔여 길이를 계산해 이어서 등록해야 합니다.
중복 등록 방지
if (!sending.exchange(true)) {
RegisterSend();
}
- 이미 전송 중이면 큐 적재만 하고 등록은 기존 루프에 맡깁니다.
Disconnect 구현 원칙
idempotent 보장
Disconnect()는 여러 번 호출돼도 한 번만 의미 있게 동작해야 합니다.
- 상태 전이를
Connected -> Disconnecting -> Disconnected로 고정하면 안전합니다.
순서
- 신규 Register 차단
- 소켓 shutdown/close
- 늦게 오는 완료 처리
OnDisconnected 1회 호출
재진입 주의
OnDisconnected에서 다시 Disconnect()를 호출해도 문제 없게 설계해야 합니다.
자주 나는 버그와 점검표
| 버그 | 증상 | 원인 |
|---|
| Recv 재등록 누락 | 어느 순간부터 수신 정지 | ProcessRecv 말미 Register 빠짐 |
| Send 중복 등록 | 패킷 순서 꼬임/중복 전송 | 전송 중 플래그 부재 |
| 잘못된 processedLen | 패킷 파싱 붕괴 | OnRecv 계약 위반 |
| 종료 중 콜백 레이스 | 간헐 크래시 | 상태 전이/수명 정책 미흡 |
운영 지표
- 세션당 outstanding Recv 수
- 세션별 송신 큐 길이
- 초당 Disconnect 사유 코드 분포
OnRecv 파싱 실패 횟수
강의 시 유의사항
강조 포인트
- Session 구현의 본질은 "성공 경로"보다 "실패/종료 경로를 끊기지 않게 잇는 것"입니다.
- Recv는 재등록 루프, Send는 큐 루프라는 차이를 분명히 가르치세요.
- Part 9(ReceiveBuffer)와 연결해
OnRecv 반환 계약을 반드시 강조하세요.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| Process 함수는 완료 알림만 처리하면 된다 | 재등록/정리/상태 전이까지 포함해야 한다 |
| Send는 호출할 때마다 바로 Register하면 된다 | 중복 등록 방지 플래그/큐 설계가 필요하다 |
OnRecv가 아무 값이나 반환해도 된다 | 커서 일관성 규칙을 깨면 즉시 장애로 이어진다 |
체크 질문 (스스로 답해보기)
numBytes == 0 수신 시 Session은 어떤 상태 전이와 정리 순서를 따라야 하는가?
- Send 큐에서 중복 Register를 막지 않으면 어떤 문제(순서/성능)가 생기는가?
OnRecv가 dataSize보다 큰 값을 반환했을 때 왜 즉시 종료해야 하는가?