using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using LiteNetLib; using LiteNetLib.Utils; using Serilog; using ServerLib.Packet; namespace ServerLib.Service; /// /// 네트워킹 추상 베이스 (protobuf 없음) /// /// 흐름: /// OnPeerConnected → 대기 목록 등록 /// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(8byte long) 읽어 인증 /// → 이미 같은 HashKey 세션 있으면 이전 피어 끊고 재연결 (WiFi→LTE) /// → 그 외 패킷은 HandlePacket() 으로 전달 /// OnPeerDisconnected → 세션/대기 목록에서 제거 /// /// 서브클래스 구현: /// OnSessionConnected - 인증 완료 시 /// OnSessionDisconnected - 세션 정상 해제 시 (재연결 교체는 호출 안 함) /// HandlePacket - 인증된 피어의 게임 패킷 처리 /// public abstract class ServerBase : INetEventListener { protected NetManager netManager = null!; // 인증 전 대기 피어 (peer.Id → NetPeer) protected readonly Dictionary pendingPeers = new(); // 인증된 세션 (hashKey → NetPeer) 재연결 조회용 // peer → hashKey 역방향은 peer.Tag as Session 으로 대체 protected readonly Dictionary sessions = new(); // Token / HashKey 관리 protected readonly Dictionary tokenHash = new(); // 재사용 NetDataWriter (단일 스레드 폴링이므로 안전) private readonly NetDataWriter cachedWriter = new(); // 핑 로그 출력 여부 public bool PingLogRtt { get; set; } public int Port { get; } public string ConnectionString { get; } private volatile bool isListening = false; public ServerBase(int port, string connectionString) { Port = port; ConnectionString = connectionString; } public virtual void Init(int pingInterval = 3000, int disconnectTimeout = 60000) { netManager = new NetManager(this) { AutoRecycle = true, PingInterval = pingInterval, DisconnectTimeout = disconnectTimeout, }; isListening = true; } public async Task Run() { netManager.Start(Port); Log.Information("[Server] 시작 Port={Port}", Port); while (isListening) { netManager.PollEvents(); await Task.Delay(1); } netManager.Stop(); Log.Information("[Server] 종료 Port={Port}", Port); } public void Stop() { isListening = false; } // 클라이언트 연결 요청 수신 → Accept / Reject 결정 public void OnConnectionRequest(ConnectionRequest request) { // 벤 기능 추가? 한국 ip만? if (request.AcceptIfKey(ConnectionString) == null) { Log.Debug("해당 클라이언트의 ConnectionKey가 동일하지 않습니다. Data={Data}", request.Data.ToString()); } } // 클라이언트가 연결 완료됐을 때 호출 public void OnPeerConnected(NetPeer peer) { pendingPeers[peer.Id] = peer; Log.Debug("[Server] 대기 등록 PeerId={Id} IP={IP}", peer.Id, peer.Address); } // 클라이언트가 연결 해제됐을 때 (타임아웃, 명시적 끊기 등) public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { pendingPeers.Remove(peer.Id); if (peer.Tag is Session session) { // 현재 인증된 피어가 이 peer일 때만 세션 제거 // (재연결로 이미 교체된 경우엔 건드리지 않음) if (sessions.TryGetValue(session.HashKey, out NetPeer? current) && current.Id == peer.Id) { // 더미 클라 아니면 token관리 if (!string.IsNullOrEmpty(session.Token)) { tokenHash.Remove(session.Token); } sessions.Remove(session.HashKey); Log.Information("[Server] 세션 해제 HashKey={Key} Reason={Reason}", session.HashKey, disconnectInfo.Reason); OnSessionDisconnected(peer, session.HashKey, disconnectInfo); } peer.Tag = null; } } // 연결된 피어로부터 데이터 수신 시 핵심 콜백 public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { byte[] data = reader.GetRemainingBytes(); try { (ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data); // Deserialize 실패 시 payload가 null if (payload == null) { Log.Warning("[Server] 패킷 역직렬화 실패 PeerId={Id} DataLen={Len}", peer.Id, data.Length); return; } // 0이라면 에코 서버 테스트용 따로 처리 if (type == 1000) { HandleEcho(peer, data); return; } // Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요) if (type == (ushort)PacketType.ACC_TOKEN) { HandleAuth(peer, payload); return; } else if (type == (ushort)PacketType.DUMMY_ACC_TOKEN) { HandleAuthDummy(peer, payload); return; } // 인증된 피어인지 확인 if (peer.Tag is not Session session) { // 추가로 벤 때려도 될듯 Log.Warning("[Server] 미인증 패킷 무시 PeerId={Id} Type={Type}", peer.Id, type); return; } // 패킷 레이트 리미팅 체크 if (session.CheckRateLimit()) { // 3회 연속 초과 시 강제 연결 해제 if (session.RateLimitViolations >= 3) { Log.Warning("[Server] 레이트 리밋 초과 강제 해제 HashKey={Key} PeerId={Id}", session.HashKey, peer.Id); peer.Disconnect(); return; } Log.Warning("[Server] 레이트 리밋 초과 ({Count}회) HashKey={Key} PeerId={Id}", session.RateLimitViolations, session.HashKey, peer.Id); return; // 패킷 드롭 } HandlePacket(peer, session.HashKey, type, payload); } catch (Exception ex) { Log.Error(ex, "[Server] 패킷 처리 오류 PeerId={Id}", peer.Id); } } // 소켓 레벨 오류 발생 시 public void OnNetworkError(IPEndPoint endPoint, SocketError socketError) { Log.Error("[Server] 네트워크 오류 {EP} {Err}", endPoint, socketError); } // 미연결 상태의 UDP 메시지 수신 (LAN 탐색, 브로드캐스트 등) public void OnNetworkReceiveUnconnected(IPEndPoint endPoint, NetPacketReader reader, UnconnectedMessageType messageType) { // 혹시나 외부에서 이벤트 발생관련 수신이라면 여기에 구현? 경험치 배율 이런거 Log.Warning("[Server] 미연결 패킷 수신 {EP} 무시", endPoint); } // 핑 갱신 시 (ms) public void OnNetworkLatencyUpdate(NetPeer peer, int latency) { if (PingLogRtt) { // rtt 시간 출력 } } // Echo 서버 테스트 protected abstract void HandleEcho(NetPeer peer, byte[] payload); // ─── Auth 처리 (더미) ──────────────────────────────────────────────── protected abstract void HandleAuthDummy(NetPeer peer, byte[] payload); // ─── Auth 처리 ──────────────────────────────────────────────── protected abstract void HandleAuth(NetPeer peer, byte[] payload); // ─── 전송 헬퍼 ─────────────────────────────────────────────────────── // peer에게 전송 protected void SendTo(NetPeer peer, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { cachedWriter.Reset(); cachedWriter.Put(data); peer.Send(cachedWriter, method); } // 모두에게 전송 protected void Broadcast(byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { cachedWriter.Reset(); cachedWriter.Put(data); netManager.SendToAll(cachedWriter, 0, method); } // exclude 1개 제외 / 나 제외 정도 protected void BroadcastExcept(byte[] data, NetPeer exclude, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { cachedWriter.Reset(); cachedWriter.Put(data); netManager.SendToAll(cachedWriter, 0, method, exclude); } // ─── 서브클래스 구현 ───────────────────────────────────────────────── // 인증(Auth) 완료 후 호출 protected abstract void OnSessionConnected(NetPeer peer, long hashKey); // 세션 정상 해제 시 호출 (재연결 교체 시에는 호출되지 않음) protected abstract void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info); // 인증된 피어의 게임 패킷 수신 / payload는 헤더 제거된 raw bytes → 실행 프로젝트에서 protobuf 역직렬화 protected abstract void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload); }