diff --git a/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs b/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs index b3994dc..1c7020f 100644 --- a/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs +++ b/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs @@ -1,17 +1,20 @@ -using LiteNetLib.Utils; +using LiteNetLib; using Serilog; namespace ClientTester.EchoDummyService; public class DummyClientService { - private readonly List _clients; - private readonly int _sendInterval; + private readonly List clients; + private readonly int sendInterval; + + // 모든거 강종 + public event Action? OnAllDisconnected; public DummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000) { - _sendInterval = sendIntervalMs; - _clients = Enumerable.Range(0, count) + sendInterval = sendIntervalMs; + clients = Enumerable.Range(0, count) .Select(i => new DummyClients(i, ip, port, key)) .ToList(); @@ -30,62 +33,100 @@ public class DummyClientService { while (!ct.IsCancellationRequested) { - foreach (var c in _clients) + foreach (DummyClients c in clients) + { c.PollEvents(); + } - try { await Task.Delay(15, ct); } - catch (OperationCanceledException) { break; } + try + { + await Task.Delay(15, ct); + } + catch (OperationCanceledException) + { + break; + } } } private async Task SendLoopAsync(CancellationToken ct) { - try { await Task.Delay(500, ct); } - catch (OperationCanceledException) { return; } + try + { + await Task.Delay(500, ct); + } + catch (OperationCanceledException) + { + return; + } int tick = 0; while (!ct.IsCancellationRequested) { int sent = 0; + int total = clients.Count; - foreach (var client in _clients) + foreach (DummyClients client in clients) { client.SendPing(); - if (client.Peer != null) sent++; + if (client.peer != null) + { + sent++; + } + else + { + total--; + } } - Log.Information("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, _clients.Count); + if (total == 0) + { + Log.Information("All Disconnect Clients"); + OnAllDisconnected?.Invoke(); + break; + } + + Log.Information("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, total); tick++; - try { await Task.Delay(_sendInterval, ct); } - catch (OperationCanceledException) { break; } + try + { + await Task.Delay(sendInterval, ct); + } + catch (OperationCanceledException) + { + break; + } } } public void PrintStats() { - int totalSent = 0, totalRecv = 0; - int connected = 0; + int totalSent = 0, totalRecv = 0; + int connected = 0; - Log.Information("════════════ Performance Report ════════════"); + Log.Information("───────────── Performance Report ─────────────"); double totalAvgRtt = 0; - foreach (var c in _clients) + foreach (DummyClients c in clients) { - var stats = c.Peer?.Statistics; - long loss = stats?.PacketLoss ?? 0; + NetStatistics? stats = c.peer?.Statistics; + long loss = stats?.PacketLoss ?? 0; float lossPct = stats?.PacketLossPercent ?? 0f; Log.Information( "[Client {ClientId:00}] Sent={Sent} Recv={Recv} | Loss={Loss}({LossPct:F1}%) AvgRTT={AvgRtt:F3}ms LastRTT={LastRtt:F3}ms", - c.ClientId, c.SentCount, c.ReceivedCount, loss, lossPct, c.AvgRttMs, c.LastRttMs); + c.clientId, c.SentCount, c.ReceivedCount, loss, lossPct, c.AvgRttMs, c.LastRttMs); - totalSent += c.SentCount; - totalRecv += c.ReceivedCount; - totalAvgRtt += c.AvgRttMs; - if (c.Peer != null) connected++; + totalSent += c.SentCount; + totalRecv += c.ReceivedCount; + totalAvgRtt += c.AvgRttMs; + if (c.peer != null) + { + connected++; + } } double avgRtt = connected > 0 ? totalAvgRtt / connected : 0; @@ -93,14 +134,16 @@ public class DummyClientService Log.Information("────────────────────────────────────────────"); Log.Information( "[TOTAL] Sent={Sent} Recv={Recv} Connected={Connected}/{Total} AvgRTT={AvgRtt:F3}ms", - totalSent, totalRecv, connected, _clients.Count, avgRtt); - Log.Information("════════════════════════════════════════════"); + totalSent, totalRecv, connected, clients.Count, avgRtt); + Log.Information("────────────────────────────────────────────"); } public void Stop() { - foreach (var c in _clients) + foreach (DummyClients c in clients) + { c.Stop(); + } Log.Information("[SERVICE] 모든 클라이언트 종료됨."); } diff --git a/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs b/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs index 8af7723..6dec658 100644 --- a/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs +++ b/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using ClientTester.Packet; using LiteNetLib; using LiteNetLib.Utils; using Serilog; @@ -7,82 +8,112 @@ namespace ClientTester.EchoDummyService; public class DummyClients { - public NetManager Manager; - public EventBasedNetListener Listener; - public NetPeer? Peer; - public int ClientId; + public NetManager manager; + public EventBasedNetListener listener; + public NetPeer? peer; + public int clientId; // seq → 송신 타임스탬프 (Stopwatch tick) - private readonly Dictionary _pendingPings = new(); - private int _seqNumber; + private readonly Dictionary pendingPings = new(); + private int seqNumber; // 통계 - public int SentCount; - public int ReceivedCount; - public double LastRttMs; - public double TotalRttMs; - public int RttCount; + public int SentCount + { + set; + get; + } + public int ReceivedCount + { + set; + get; + } + public double LastRttMs + { + set; + get; + } + public double TotalRttMs + { + set; + get; + } + public int RttCount + { + set; + get; + } public DummyClients(int clientId, string ip, int port, string key) { - ClientId = clientId; - Listener = new EventBasedNetListener(); - Manager = new NetManager(Listener); + this.clientId = clientId; + listener = new EventBasedNetListener(); + manager = new NetManager(listener); - Listener.PeerConnectedEvent += peer => + listener.PeerConnectedEvent += netPeer => { - Peer = peer; - Log.Information("[Client {ClientId:00}] 연결됨", ClientId); + peer = netPeer; + Log.Information("[Client {ClientId:00}] 연결됨", this.clientId); }; - Listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) => + listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) => { - var msg = reader.GetString(); + string? msg = reader.GetString(); - // "ack:seq:{seqNum}" 파싱 - var parts = msg.Split(':'); - if (parts.Length == 3 && parts[0] == "ack" && parts[1] == "seq" - && int.TryParse(parts[2], out int seq) - && _pendingPings.TryGetValue(seq, out long sentTick)) + string[] parts = msg.Split(':'); + if (parts.Length == 3 && parts[0] == "ack" && parts[1] == "seq" && int.TryParse(parts[2], out int seq) && pendingPings.TryGetValue(seq, out long sentTick)) { - double rttMs = (Stopwatch.GetTimestamp() - sentTick) - * 1000.0 / Stopwatch.Frequency; + double rttMs = (Stopwatch.GetTimestamp() - sentTick) * 1000.0 / Stopwatch.Frequency; LastRttMs = rttMs; TotalRttMs += rttMs; RttCount++; - _pendingPings.Remove(seq); + pendingPings.Remove(seq); } ReceivedCount++; reader.Recycle(); }; - Listener.PeerDisconnectedEvent += (peer, info) => + listener.PeerDisconnectedEvent += (peer, info) => { - Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", ClientId, info.Reason); - Peer = null; + Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", this.clientId, info.Reason); + this.peer = null; }; - Manager.Start(); - Manager.Connect(ip, port, key); + manager.Start(); + manager.Connect(ip, port, key); } public void SendPing() { - if (Peer is null) return; + if (peer == null) + { + return; + } - int seq = _seqNumber++; - _pendingPings[seq] = Stopwatch.GetTimestamp(); + int seq = seqNumber++; + pendingPings[seq] = Stopwatch.GetTimestamp(); - var writer = new NetDataWriter(); - writer.Put($"seq:{seq}"); - Peer.Send(writer, DeliveryMethod.ReliableOrdered); + NetDataWriter writer = new NetDataWriter(); + PacketHeader packetHeader = new PacketHeader(); + packetHeader.Code = 0; + packetHeader.BodyLength = $"seq:{seq}".Length; + writer.Put((short)packetHeader.Code); + writer.Put((short)packetHeader.BodyLength); + writer.Put($"Echo seq:{seq}"); + peer.Send(writer, DeliveryMethod.ReliableOrdered); SentCount++; } public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0; - public void PollEvents() => Manager.PollEvents(); + public void PollEvents() + { + manager.PollEvents(); + } - public void Stop() => Manager.Stop(); + public void Stop() + { + manager.Stop(); + } } diff --git a/ClientTester/EchoClientTester/Packet/PacketBody.cs b/ClientTester/EchoClientTester/Packet/PacketBody.cs index a3fbcc1..e03683d 100644 --- a/ClientTester/EchoClientTester/Packet/PacketBody.cs +++ b/ClientTester/EchoClientTester/Packet/PacketBody.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS8618 + using ProtoBuf; namespace ClientTester.Packet; @@ -164,7 +166,7 @@ public class IntoLobbyPacket { get; set; - } + } = null!; } // EXIT_LOBBY diff --git a/ClientTester/EchoClientTester/Packet/PacketHeader.cs b/ClientTester/EchoClientTester/Packet/PacketHeader.cs index b95aa68..3512a8b 100644 --- a/ClientTester/EchoClientTester/Packet/PacketHeader.cs +++ b/ClientTester/EchoClientTester/Packet/PacketHeader.cs @@ -1,55 +1,55 @@ namespace ClientTester.Packet; -public enum PacketCode +public enum PacketCode : short { NONE, // 초기 클라이언트 시작시 jwt토큰 받아옴 RECV_TOKEN, // jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송) LOAD_GAME, - + // 마을(로비)진입시 모든 데이터 로드 INTO_LOBBY, - + // 로비 나가기 EXIT_LOBBY, - + // 인스턴스 던전 입장 INTO_INSTANCE, - + // 결과 보상 REWARD_INSTANCE, - + // 보스전 (시작, 종료) UPDATE_BOSS, - + // 인스턴스 던전 퇴장 EXIT_INSTANCE, - + // 파티 (생성, 삭제) UPDATE_PARTY, - + // 파티 유저 업데이트(추가 삭제) UPDATE_USER_PARTY, - + // 플레이어 위치, 방향 TRANSFORM_PLAYER, - + // 플레이어 행동 업데이트 ACTION_PLAYER, - + // 플레이어 스테이트 업데이트 STATE_PLAYER, - + // NPC 위치, 방향 TRANSFORM_NPC, - + // NPC 행동 업데이트 ACTION_NPC, - + // NPC 스테이트 업데이트 STATE_NPC, - + // 데미지 UI 전달 DAMAGE } @@ -58,4 +58,4 @@ public class PacketHeader { public PacketCode Code; public int BodyLength; -} \ No newline at end of file +} diff --git a/ClientTester/EchoClientTester/Program.cs b/ClientTester/EchoClientTester/Program.cs index 73aed81..c57acc1 100644 --- a/ClientTester/EchoClientTester/Program.cs +++ b/ClientTester/EchoClientTester/Program.cs @@ -1,28 +1,37 @@ using ClientTester.EchoDummyService; using Serilog; -Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger(); - -const string SERVER_IP = "localhost"; -const int SERVER_PORT = 9500; -const string CONNECTION_KEY = "game"; -const int CLIENT_COUNT = 1000; - -var cts = new CancellationTokenSource(); -Console.CancelKeyPress += (_, e) => +class EcoClientTester { - e.Cancel = true; - Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); - cts.Cancel(); -}; + public static readonly string SERVER_IP = "localhost"; + public static readonly int SERVER_PORT = 9500; + public static readonly string CONNECTION_KEY = "test"; + public static readonly int CLIENT_COUNT = 100; -var service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); -await service.RunAsync(cts.Token); + CancellationTokenSource cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); + cts.Cancel(); + }; -service.PrintStats(); -service.Stop(); + DummyClientService service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); + service.OnAllDisconnected += async () => + { + Log.Warning("[SHUTDOWN] 종료 이벤트 발생, 종료 중..."); + await cts.CancelAsync(); + }; -Log.CloseAndFlush(); + await service.RunAsync(cts.Token); + + service.PrintStats(); + service.Stop(); + + await Log.CloseAndFlushAsync(); + } +} diff --git a/MMOTestServer/MMOserver/MMOserver.csproj b/MMOTestServer/MMOserver/MMOserver.csproj index 4fde807..ddb6123 100644 --- a/MMOTestServer/MMOserver/MMOserver.csproj +++ b/MMOTestServer/MMOserver/MMOserver.csproj @@ -6,6 +6,7 @@ enable enable Linux + 13 diff --git a/MMOTestServer/MMOserver/Packet/PacketBody.cs b/MMOTestServer/MMOserver/Packet/PacketBody.cs index a3fbcc1..6cc4cac 100644 --- a/MMOTestServer/MMOserver/Packet/PacketBody.cs +++ b/MMOTestServer/MMOserver/Packet/PacketBody.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS8618 + using ProtoBuf; -namespace ClientTester.Packet; +namespace MMOserver.Packet; // ============================================================ // 공통 타입 diff --git a/MMOTestServer/MMOserver/Packet/PacketHeader.cs b/MMOTestServer/MMOserver/Packet/PacketHeader.cs index b95aa68..f0c927c 100644 --- a/MMOTestServer/MMOserver/Packet/PacketHeader.cs +++ b/MMOTestServer/MMOserver/Packet/PacketHeader.cs @@ -1,55 +1,55 @@ -namespace ClientTester.Packet; +namespace MMOserver.Packet; -public enum PacketCode +public enum PacketCode : short { NONE, // 초기 클라이언트 시작시 jwt토큰 받아옴 RECV_TOKEN, // jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송) LOAD_GAME, - + // 마을(로비)진입시 모든 데이터 로드 INTO_LOBBY, - + // 로비 나가기 EXIT_LOBBY, - + // 인스턴스 던전 입장 INTO_INSTANCE, - + // 결과 보상 REWARD_INSTANCE, - + // 보스전 (시작, 종료) UPDATE_BOSS, - + // 인스턴스 던전 퇴장 EXIT_INSTANCE, - + // 파티 (생성, 삭제) UPDATE_PARTY, - + // 파티 유저 업데이트(추가 삭제) UPDATE_USER_PARTY, - + // 플레이어 위치, 방향 TRANSFORM_PLAYER, - + // 플레이어 행동 업데이트 ACTION_PLAYER, - + // 플레이어 스테이트 업데이트 STATE_PLAYER, - + // NPC 위치, 방향 TRANSFORM_NPC, - + // NPC 행동 업데이트 ACTION_NPC, - + // NPC 스테이트 업데이트 STATE_NPC, - + // 데미지 UI 전달 DAMAGE } @@ -58,4 +58,4 @@ public class PacketHeader { public PacketCode Code; public int BodyLength; -} \ No newline at end of file +} diff --git a/MMOTestServer/MMOserver/Program.cs b/MMOTestServer/MMOserver/Program.cs index 261bbe4..54c3062 100644 --- a/MMOTestServer/MMOserver/Program.cs +++ b/MMOTestServer/MMOserver/Program.cs @@ -1,6 +1,8 @@ using MMOserver.Game; using Serilog; +namespace MMOserver; + class Program { private static void Main() diff --git a/MMOTestServer/ServerLib/Packet/PacketSerializer.cs b/MMOTestServer/ServerLib/Packet/PacketSerializer.cs index 1feebbb..9f9cb3f 100644 --- a/MMOTestServer/ServerLib/Packet/PacketSerializer.cs +++ b/MMOTestServer/ServerLib/Packet/PacketSerializer.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using ProtoBuf; using Serilog; diff --git a/MMOTestServer/ServerLib/ServerLib.csproj b/MMOTestServer/ServerLib/ServerLib.csproj index 38c79cd..1da3505 100644 --- a/MMOTestServer/ServerLib/ServerLib.csproj +++ b/MMOTestServer/ServerLib/ServerLib.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + 13 diff --git a/MMOTestServer/ServerLib/Service/ServerBase.cs b/MMOTestServer/ServerLib/Service/ServerBase.cs index 81bcdc1..95885b1 100644 --- a/MMOTestServer/ServerLib/Service/ServerBase.cs +++ b/MMOTestServer/ServerLib/Service/ServerBase.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Threading; using LiteNetLib; using LiteNetLib.Utils; using Serilog; @@ -122,6 +125,13 @@ public abstract class ServerBase : INetEventListener { (ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data); + // 0이라면 에코 서버 테스트용 따로 처리 + if (type == 0) + { + HandleEcho(peer, payload); + return; + } + // Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요) if (type == (ushort)PacketType.Auth) { @@ -167,16 +177,33 @@ public abstract class ServerBase : INetEventListener } } + // Echo 서버 테스트 + + private void HandleEcho(NetPeer peer, byte[] payload) + { + // if (payload.Length < sizeof(long)) + // { + // Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id}", peer.Id); + // peer.Disconnect(); + // return; + // } + + // 세션에 넣지는 않는다. + NetDataReader reader = new NetDataReader(payload); + Log.Debug("[Echo] : addr={Addr}, str={Str}", peer.Address, reader.GetString()); + SendTo(peer, payload); + } + // ─── Auth 처리 (내부) ──────────────────────────────────────────────── private void HandleAuth(NetPeer peer, byte[] payload) { - if (payload.Length < sizeof(long)) - { - Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id); - peer.Disconnect(); - return; - } + // if (payload.Length < sizeof(long)) + // { + // Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id); + // peer.Disconnect(); + // return; + // } long hashKey = BitConverter.ToInt64(payload, 0); diff --git a/MMOTestServer/ServerLib/Service/SessionManager.cs b/MMOTestServer/ServerLib/Service/SessionManager.cs index 49cb4a8..6b16bd8 100644 --- a/MMOTestServer/ServerLib/Service/SessionManager.cs +++ b/MMOTestServer/ServerLib/Service/SessionManager.cs @@ -1,4 +1,6 @@ -namespace ServerLib.Service; +using System.Collections.Generic; + +namespace ServerLib.Service; public class SessionManager {