290 lines
9.7 KiB
C#
290 lines
9.7 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;
|
|
}
|
|
|
|
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);
|
|
}
|