Files
a301_mmo_game_server/MMOTestServer/ServerLib/Service/ServerBase.cs
2026-04-03 01:38:36 +09:00

291 lines
9.8 KiB
C#

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;
/// <summary>
/// 네트워킹 추상 베이스 (protobuf 없음)
///
/// 흐름:
/// OnPeerConnected → 대기 목록 등록
/// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(4byte int) 읽어 인증
/// → 이미 같은 HashKey 세션 있으면 이전 피어 끊고 재연결 (WiFi→LTE)
/// → 그 외 패킷은 HandlePacket() 으로 전달
/// OnPeerDisconnected → 세션/대기 목록에서 제거
///
/// 서브클래스 구현:
/// OnSessionConnected - 인증 완료 시
/// OnSessionDisconnected - 세션 정상 해제 시 (재연결 교체는 호출 안 함)
/// HandlePacket - 인증된 피어의 게임 패킷 처리
/// </summary>
public abstract class ServerBase : INetEventListener
{
protected NetManager netManager = null!;
// 인증 전 대기 피어 (peer.Id → NetPeer)
protected readonly Dictionary<int, NetPeer> pendingPeers = new();
// 인증된 세션 (hashKey → NetPeer) 재연결 조회용
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
protected readonly Dictionary<int, NetPeer> sessions = new();
// Token / HashKey 관리
protected readonly Dictionary<string, int> tokenHash = new();
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
private readonly NetDataWriter cachedWriter = new();
// async 메서드(HandleAuth 등)의 await 이후 공유 자원 접근 보호용
protected readonly object sessionLock = new();
// 핑 로그 출력 여부
public bool PingLogRtt
{
get;
set;
}
public int Port
{
get;
}
public string ConnectionString
{
get;
}
// 최적화 되면 안되므로 volatile로 선언
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)
{
lock (sessionLock)
{
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).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "[Server] HandleAuth 예외 PeerId={Id}", peer.Id);
}
}, TaskContinuationOptions.OnlyOnFaulted);
return;
}
// Auth 이외 패킷 처리
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 시간 출력
// Log.Debug("[Server] latency : {Latency} ", latency);
}
}
// Echo 서버 테스트
protected abstract void HandleEcho(NetPeer peer, byte[] payload);
// Auth 처리 (더미)
protected abstract void HandleAuthDummy(NetPeer peer, byte[] payload);
// Auth 처리
protected abstract Task 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, int hashKey);
// 세션 정상 해제 시 호출 (재연결 교체 시에는 호출되지 않음)
protected abstract void OnSessionDisconnected(NetPeer peer, int hashKey, DisconnectInfo info);
// 인증된 피어의 게임 패킷 수신 / payload는 헤더 제거된 raw bytes → 실행 프로젝트에서 protobuf 역직렬화
protected abstract void HandlePacket(NetPeer peer, int hashKey, ushort type, byte[] payload);
}