using System.Net; using System.Net.Sockets; 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) private readonly Dictionary pendingPeers = new(); // 인증된 세션 (hashKey → NetPeer) 재연결 조회용 // peer → hashKey 역방향은 peer.Tag as Session 으로 대체 private readonly Dictionary sessions = new(); // 핑 로그 출력 여부 public bool PingLogRtt { get; set; } public int Port { get; } public string ConnectionString { get; } private 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 virtual void Run() { netManager.Start(Port); Log.Information("[Server] 시작 Port={Port}", Port); while (isListening) { netManager.PollEvents(); Thread.Sleep(15); } 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={request.ConnectionKey}가 동일하지 않습니다", 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) { 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); // Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요) if (type == (ushort)PacketType.Auth) { HandleAuth(peer, payload); return; } // 인증된 피어인지 확인 if (peer.Tag is not Session session) { // 추가로 벤 때려도 될듯 Log.Warning("[Server] 미인증 패킷 무시 PeerId={Id} Type={Type}", peer.Id, type); 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 시간 출력 } } // ─── Auth 처리 (내부) ──────────────────────────────────────────────── private void HandleAuth(NetPeer peer, byte[] payload) { if (payload.Length < sizeof(long)) { Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id); peer.Disconnect(); return; } long hashKey = BitConverter.ToInt64(payload, 0); if (sessions.TryGetValue(hashKey, out NetPeer? existing)) { // WiFi → LTE 전환 등 재연결: 이전 피어 교체 existing.Tag = null; sessions.Remove(hashKey); Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); existing.Disconnect(); } peer.Tag = new Session(hashKey, peer); sessions[hashKey] = peer; pendingPeers.Remove(peer.Id); Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); } // ─── 전송 헬퍼 ─────────────────────────────────────────────────────── // NetDataWriter writer 풀처리 필요할듯 // peer에게 전송 protected void SendTo(NetPeer peer, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { NetDataWriter writer = new NetDataWriter(); writer.Put(data); peer.Send(writer, method); } // 모두에게 전송 protected void Broadcast(byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { // 일단 channelNumber는 건드리지 않는다 NetDataWriter writer = new NetDataWriter(); writer.Put(data); netManager.SendToAll(writer, 0, method); } // exclude 1개 제외 / 나 제외 정도 protected void BroadcastExcept(byte[] data, NetPeer exclude, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { // 일단 channelNumber는 건드리지 않는다 NetDataWriter writer = new NetDataWriter(); writer.Put(data); netManager.SendToAll(writer, 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); }