240 lines
8.7 KiB
C#
240 lines
8.7 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using LiteNetLib;
|
|
using LiteNetLib.Utils;
|
|
using Serilog;
|
|
using ServerLib.Packet;
|
|
|
|
namespace ServerLib.Service;
|
|
|
|
/// <summary>
|
|
/// 네트워킹 추상 베이스 (protobuf 없음)
|
|
///
|
|
/// 흐름:
|
|
/// OnPeerConnected → 대기 목록 등록
|
|
/// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(8byte long) 읽어 인증
|
|
/// → 이미 같은 HashKey 세션 있으면 이전 피어 끊고 재연결 (WiFi→LTE)
|
|
/// → 그 외 패킷은 HandlePacket() 으로 전달
|
|
/// OnPeerDisconnected → 세션/대기 목록에서 제거
|
|
///
|
|
/// 서브클래스 구현:
|
|
/// OnSessionConnected - 인증 완료 시
|
|
/// OnSessionDisconnected - 세션 정상 해제 시 (재연결 교체는 호출 안 함)
|
|
/// HandlePacket - 인증된 피어의 게임 패킷 처리
|
|
/// </summary>
|
|
public abstract class ServerBase : INetEventListener
|
|
{
|
|
protected NetManager netManager = null!;
|
|
|
|
// 인증 전 대기 피어 (peer.Id → NetPeer)
|
|
private readonly Dictionary<int, NetPeer> pendingPeers = new();
|
|
|
|
// 인증된 세션 (hashKey → NetPeer) 재연결 조회용
|
|
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
|
|
private readonly Dictionary<long, NetPeer> 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);
|
|
}
|