Gateway에서 Singleton SessionMapper 대신 각 GatewaySession 객체가 자신의 라우팅 정보를 메모리에 저장하도록 변경
-
새 필드 추가:
PinnedGameServerNodeId: 고정된 GameServer NodeId (0 = 미고정)GameSessionId: 게임 세션 ID (0 = 미할당)_lock: 스레드 안전성을 위한 lock 객체
-
새 메서드 추가:
Pin(gameServerNodeId, gameSessionId): 세션을 특정 GameServer에 고정Reroute(newTargetNodeId): 다른 GameServer로 재라우팅Unpin(): 세션 고정 해제
-
OnReceived 수정:
- 세션 객체에서 직접
PinnedGameServerNodeId와GameSessionId를 읽어서 전달 - Lock-Free 읽기로 성능 최적화
- 세션 객체에서 직접
-
OnDisconnected 수정:
- 연결 종료 시
Unpin()호출하여 세션 정보 정리
- 연결 종료 시
-
ForwardToGameServer 시그니처 변경:
// 이전: ForwardToGameServer(Guid socketId, byte[] clientData) // 이후: ForwardToGameServer(Guid socketId, byte[] clientData, long pinnedNodeId, long sessionId)
- 세션 객체에서 라우팅 정보를 받아서 처리
pinnedNodeId > 0이면 해당 GameServer로 직접 전송pinnedNodeId == 0이면 라운드 로빈으로 선택
-
HandleControlPacket 개선:
PinSession: 세션 객체의Pin()메서드 호출 (SessionMapper는 하위 호환용으로만 유지)DisconnectClient: 기존 로직 유지RerouteSocket: 새로 추가된 제어 명령 처리
- ControlCommand enum에 추가:
RerouteSocket = 3: 재라우팅 제어 명령 (재접속, 중복 로그인 처리용)
- GameServerConnector.cs 제거: 더 이상 사용되지 않는 레거시 코드 삭제
- Program.cs 수정: ILoggerFactory 주입으로 GatewayTcpServer용 Logger 생성
- 각 GatewaySession이 자신의 라우팅 정보를 메모리에 저장
- 읽기 작업은 Lock-Free (성능 향상)
- 쓰기 작업만 Lock 사용 (PinSession, Reroute 등)
- 성능: Singleton Dictionary 조회 없이 세션 객체에서 직접 읽기
- 확장성: 세션별로 독립적인 메모리 관리
- 단순성: 중앙 집중식 SessionMapper 의존성 감소
- 스레드 안전: Lock을 세션 단위로 사용하여 경합 최소화
✅ GatewayServer.csproj - Build SUCCESS (0 warnings, 0 errors)
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)Priority 3: Service Mesh Kick-out Protocol 구현 시작
- ISessionStore 인터페이스 정의
- Redis 연동 (StackExchange.Redis)
- 중복 로그인 감지 및 처리
-
SessionInfo record: Redis에 저장될 세션 정보 구조
- GameServerNodeId, SessionId, GatewayNodeId, SocketId
- CreatedAtUtc, LastActivityUtc
-
인터페이스 메서드:
GetSessionAsync(userId): 세션 정보 조회SetSessionAsync(userId, sessionInfo, ttl): 세션 저장/갱신DeleteSessionAsync(userId): 세션 삭제IsSessionOnNodeAsync(userId, nodeId): 특정 노드 세션 확인
- StackExchange.Redis 활용: IConnectionMultiplexer 기반
- Key 형식:
session:{userId} - 기본 TTL: 24시간
- JSON 직렬화: System.Text.Json 사용
- 에러 처리: 모든 Redis 작업에 try-catch 및 로깅
-
중복 로그인 감지:
var existingSession = await _sessionStore.GetSessionAsync(userId); if (existingSession != null) { await HandleDuplicateLogin(...); }
-
HandleDuplicateLogin 메서드:
- 같은 GameServer:
SendDisconnectClient()직접 호출 - 다른 GameServer: Service Mesh 통신 필요 (Priority 3에서 구현)
- 기존 세션 정리 후 새 세션 생성
- Redis 세션 정보 갱신
- 같은 GameServer:
-
신규 로그인:
- SessionInfo 생성
- Redis에 저장 (SSOT)
- Gateway에게 PinSession 지시
- 클라이언트에게 로그인 성공 응답
- SendDisconnectClient 메서드 추가:
- DisconnectClient 제어 패킷 전송
- 중복 로그인 시 기존 소켓 강제 종료용
- IConnectionMultiplexer 등록: Redis 연결
- ISessionStore 등록: RedisSessionStore 구현체
- Redis를 유일한 세션 정보 저장소로 사용
- 모든 GameServer가 Redis를 통해 세션 상태 동기화
- Same-Node Duplicate: 직접 P2P DisconnectClient 전송
- Cross-Node Duplicate: Service Mesh KickoutRequest 필요 (Priority 3)
- Session Cleanup: 기존 세션 제거 후 새 세션 생성
- 기본 24시간 세션 만료
- 활동 시 LastActivityUtc 갱신 (추후 구현)
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)Priority 4: Reconnect Handling 구현 시작
- 다른 GameServer에 로그인한 중복 세션 kick-out
- Service Mesh 통신 (GameServer ↔ GameServer)
- 동기식 대기 (KickoutAck 응답 받을 때까지)
-
Header 구조 (25 bytes):
- MessageType(1) + SourceNodeId(8) + RequestId(8) + PayloadSize(4) + Reserved(4)
-
MessageType enum:
KickoutRequest: GameServer → GameServer kick-out 요청KickoutAck: kick-out 완료 응답ServiceRequest/Response: Stateless Service 통신용 (향후 사용)
-
KickoutRequest Payload (40 bytes):
- UserId, SessionId, GatewayNodeId, SocketId
-
KickoutAck Payload (12 bytes):
- UserId, Success, ErrorCode
-
KickoutErrorCode enum:
- Success, UserNotFound, SessionMismatch, AlreadyDisconnected, InternalError
-
PacketBuilder:
CreateKickoutRequest(): RequestId 자동 생성CreateKickoutAck(): 응답 패킷 생성
-
NetMQ DealerSocket: Service Mesh 통신
-
Identity:
GameServer-{NodeId} -
동기식 Kick-out 요청:
public async Task<KickoutAck> SendKickoutRequestAsync( long targetNodeId, long userId, long sessionId, ...) { // RequestId 생성 및 TaskCompletionSource 등록 // 패킷 전송 // 타임아웃 5초와 함께 응답 대기 }
-
Pending Request 관리:
ConcurrentDictionary<RequestId, TaskCompletionSource<KickoutAck>>- 타임아웃: 5초
- 응답 수신 시 TaskCompletionSource.SetResult()
-
KickoutRequest 처리:
- 다른 GameServer로부터 요청 수신
- 로컬 세션 확인 및 disconnect
- KickoutAck 응답 전송
-
HandleDuplicateLogin 개선:
if (existingSession.GameServerNodeId != _config.NodeId) { // Cross-Node: Service Mesh KickoutRequest var ack = await _nodeCommunicator.SendKickoutRequestAsync(...); kickoutSuccess = ack.Success; } else { // Same-Node: 직접 DisconnectClient _p2pListener.SendDisconnectClient(...); kickoutSuccess = true; }
-
Kick-out 실패 처리:
- 실패하더라도 새 세션 생성 허용
- 로깅으로 문제 추적
- NodeCommunicator 등록: Singleton
- GameServerHostedService 수정:
- NodeCommunicator.Start() 호출
- Service Mesh 준비 로그 출력
- P2P Channel: Gateway ↔ GameServer (클라이언트 패킷)
- Service Mesh Channel: GameServer ↔ GameServer (제어 메시지)
- 두 채널은 완전히 분리됨
- RequestId로 요청-응답 매칭
- TaskCompletionSource로 비동기 대기
- 타임아웃으로 무한 대기 방지
-
Cross-Node Duplicate:
- NodeCommunicator.SendKickoutRequestAsync() 호출
- 대상 GameServer에서 DisconnectClient 전송
- KickoutAck 응답 대기 (최대 5초)
-
Same-Node Duplicate:
- GatewayPacketListener.SendDisconnectClient() 직접 호출
- 로컬 처리로 빠른 응답
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)Priority 4: Reconnect Handling (Re-pinning, Snapshot Sync)
- GameServer 패킷 처리에 AOP 적용
- Middleware Pattern으로 횡단 관심사 분리
- 확장 가능하고 테스트 가능한 구조
-
IPacketMiddleware: Middleware 인터페이스
Task InvokeAsync(PacketContext context, Func<Task> next);
-
PacketContext: 패킷 처리 컨텍스트
- Request: GatewayNodeId, SocketId, SessionId, Payload
- Response: Response, SendResponse
- 메타데이터: UserId, Opcode, StartTime, Items
- 에러: Exception, IsCompleted
- MiddlewarePipeline: Middleware 체인 관리 및 실행
- MiddlewarePipelineFactory: DI 기반 Pipeline 생성
ExceptionHandlingMiddleware (최상위):
- 전역 예외 처리 및 로깅
- 에러 응답 자동 생성
- 클라이언트에게 에러 전송
LoggingMiddleware:
- Request/Response 로깅
- AOP Cross-cutting Concern
- 예외 발생 시 로깅
PerformanceMiddleware:
- Stopwatch로 처리 시간 측정
- Slow Packet 감지 (100ms 이상 경고)
- Context.Items에 실행 시간 저장
PacketHandlerMiddleware (마지막):
- IPacketProcessor.ProcessAsync() 호출
- 실제 비즈니스 로직 실행
- IClientPacketHandler 유지: 기존 인터페이스 호환
- IPacketProcessor 구현: 비즈니스 로직 처리
- ClientPacketContext → PacketContext 변환
- Middleware Pipeline 실행
services.AddSingleton<MiddlewarePipelineFactory>();
services.AddSingleton<ExceptionHandlingMiddleware>();
services.AddSingleton<LoggingMiddleware>();
services.AddSingleton<PerformanceMiddleware>();
services.AddSingleton<PacketHandlerMiddleware>();
services.AddSingleton<GameServerHub>();
services.AddSingleton<IPacketProcessor>(sp => sp.GetRequiredService<GameServerHub>());
services.AddSingleton<IClientPacketHandler>(sp => sp.GetRequiredService<GameServerHub>());Client Packet
↓
ExceptionHandlingMiddleware (예외 처리)
↓
LoggingMiddleware (로깅)
↓
PerformanceMiddleware (성능 측정)
↓
PacketHandlerMiddleware (비즈니스 로직)
↓
Response
- 관심사 분리: 비즈니스 로직과 횡단 관심사 분리
- 재사용성: Middleware를 독립적으로 재사용 가능
- 유연성: Middleware 추가/제거/순서 변경 용이
- 테스트 용이성: 각 Middleware 독립적으로 단위 테스트
- 유지보수성: 명확한 코드 구조
- 새로운 Middleware 추가 시 기존 코드 수정 불필요
- 조건부 Middleware 실행 가능 (특정 Opcode만)
- DI를 통한 의존성 주입
- ValidationMiddleware (패킷 유효성 검사)
- CachingMiddleware (응답 캐싱)
- RateLimitingMiddleware (요청 속도 제한)
- CompressionMiddleware (패킷 압축)
- MetricsMiddleware (Prometheus 메트릭 수집)
- TracingMiddleware (분산 추적)
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)상세 내용은 AOP_MIDDLEWARE.md 참조
- 재접속 시나리오 처리
- Re-pinning 메커니즘 (동일 GameServer)
- Reroute 메커니즘 (다른 GameServer)
- Snapshot 동기화 (현재 상태만)
- SnapshotData: UserState, GameState, LastSequenceId, Version
- ISessionSnapshot: CreateSnapshotAsync, ApplySnapshotAsync
- InMemorySessionSnapshot: 임시 인메모리 구현
- HandleUnpinnedSession: 신규 로그인 vs 재접속 구분
- HandleNewLogin: 신규 로그인 로직 분리
- HandleReconnect: Same-Node / Cross-Node 분기
- ExtractLoginInfo: UserId, isReconnect, oldSessionId 추출
- 기존 SessionId 재사용
- 세션 정보 업데이트 (새 Gateway, 새 SocketId)
- Re-pinning 지시
- 스냅샷 생성 및 전송
- 새 SessionId 생성
- 새 GameServer에서 세션 생성
- 기존 상태 손실 (제약사항)
- TODO: 기존 GameServer 세션 정리
- CreateReconnectSuccessResponse: SessionId + Snapshot JSON
- Same-Node: SessionId 재사용, 상태 유지
- Cross-Node: 새 SessionId, 상태 손실
- Snapshot: 현재 상태만 (Event Sourcing 아님)
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)- Sequence ID 추가 (Idempotency)
- 중복 패킷 감지 및 무시
- 프로토콜 버전 관리
- HeaderSize 증가: 41 bytes → 49 bytes
- P2PHeader.SequenceId: 8 bytes (Idempotency용)
- ProtocolVersion: 상수 정의 (현재 버전 1)
- Serialize/Deserialize 업데이트: SequenceId 직렬화 추가
- CreateClientPacket: sequenceId 파라미터 추가 (기본값 0)
- CreateServerPacket: sequenceId 파라미터 추가 (기본값 0)
- 하위 호환성: sequenceId는 선택적 파라미터
-
GetLastSequenceIdAsync: 마지막 처리한 Sequence ID 조회
-
SetLastSequenceIdAsync: Sequence ID 업데이트
-
IsProcessedAsync: 중복 패킷 여부 확인
-
InMemorySequenceIdStore: 인메모리 구현 (Dictionary)
-
설계 변경 사항: Stateful 게임 서버 아키텍처 원칙에 따라, Redis 연동이나 프레임워크 단의 자동 Idempotency 처리는 제거됨. 재접속 및 상태 복구는 네트워크 라이브러리가 강제하지 않고 오직 게임 애플리케이션(유저) લે벨에서 필요에 따라 직접 구현하도록 인터페이스 구조만 제공.
- 상태 변경: 프레임워크에서 제거됨
- 사유: 패킷 유실 시 전체 상태를 응답하는 Stateful 구조에서는 프레임워크가 강제하는 Idempotency Pipeline이 오히려 방해가 됨. 게임 레이어에서 선택적으로 구현하도록 위임.
- SequenceId 필드 추가: P2P 헤더에서 추출
- GatewayPacketListener에서 SequenceId 설정
- PacketContext.Items에 SequenceId 추가
- IdempotencyMiddleware가 Items에서 SequenceId 추출
ExceptionHandling
↓
Logging
↓
Performance
↓
PacketHandler
RedisSequenceIdStore및IdempotencyMiddleware기본 주입 코드 완전 제거.
- At-Least-Once Delivery: 네트워크 재전송으로 인한 중복 패킷 방지
- SequenceId 검증: 각 세션별로 마지막 처리 ID 추적
- 자동 무시: 중복 패킷은 로그 후 무시, 응답 없음
- ProtocolVersion 상수: 향후 호환성 관리
- HeaderSize 명시: 버전별 헤더 크기 구분
- Redis 기반 저장: 분산 환경에서 일관성 보장
- TTL 24시간: 오래된 세션 자동 정리
- 조건부 체크: SequenceId 0이면 건너뛰기
✅ GameServer.csproj - Build SUCCESS (0 warnings, 0 errors)- ✅ Priority 1: Gateway 세션별 라우팅
- ✅ Priority 2: GameServer Redis 세션 관리
- ✅ Priority 3: Service Mesh Kick-out Protocol
- ✅ Priority 4: Reconnect Handling
- ✅ Priority 5: Protocol Extensions (Sequence ID)
- ✅ AOP Middleware Pattern
- 클라이언트 구현 (Unity/Unreal)
- 부하 테스트 및 성능 튜닝
- 모니터링 및 메트릭 수집 (Prometheus)
- 분산 추적 (OpenTelemetry)
- 프로토콜 버전 2 설계