From 9930348d5ec59bed4f01b9960e34a900dbb22c54 Mon Sep 17 00:00:00 2001 From: qornwh1 Date: Wed, 4 Mar 2026 08:55:27 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EB=8D=94=EB=AF=B8=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=ED=98=84=20/=20?= =?UTF-8?q?=EB=8D=94=EB=AF=B8=ED=81=B4=EB=9D=BC=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=97=90=EC=BD=94=EC=9A=A9,=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=EC=9A=A9=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DummyClientService.cs | 35 +- .../DummyService/DummyClients.cs | 215 +++++++++++ .../EchoDummyClientService.cs | 167 +++++++++ .../{DummyClients.cs => EchoDummyClients.cs} | 6 +- .../EchoClientTester/EchoDummyService/Map.cs | 6 + .../EchoClientTester/Packet/PacketBody.cs | 335 ++++++++---------- .../EchoClientTester/Packet/PacketHeader.cs | 71 ++-- .../Packet/PacketSerializer.cs | 7 +- ClientTester/EchoClientTester/Program.cs | 115 ++++-- ClientTester/EchoClientTester/Vector3.cs | 8 + 10 files changed, 678 insertions(+), 287 deletions(-) rename ClientTester/EchoClientTester/{EchoDummyService => DummyService}/DummyClientService.cs (84%) create mode 100644 ClientTester/EchoClientTester/DummyService/DummyClients.cs create mode 100644 ClientTester/EchoClientTester/EchoDummyService/EchoDummyClientService.cs rename ClientTester/EchoClientTester/EchoDummyService/{DummyClients.cs => EchoDummyClients.cs} (95%) create mode 100644 ClientTester/EchoClientTester/EchoDummyService/Map.cs create mode 100644 ClientTester/EchoClientTester/Vector3.cs diff --git a/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs b/ClientTester/EchoClientTester/DummyService/DummyClientService.cs similarity index 84% rename from ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs rename to ClientTester/EchoClientTester/DummyService/DummyClientService.cs index d2c3377..0c40ac8 100644 --- a/ClientTester/EchoClientTester/EchoDummyService/DummyClientService.cs +++ b/ClientTester/EchoClientTester/DummyService/DummyClientService.cs @@ -1,53 +1,27 @@ using LiteNetLib; using Serilog; -namespace ClientTester.EchoDummyService; +namespace ClientTester.DummyService; public class DummyClientService { private readonly List clients; private readonly int sendInterval; - // 유닛 테스트용 (n패킷 시간체크) - public bool IsTest - { - get; - set; - } = false; - - public int TestCount - { - get; - set; - } = 100000; - // 모든거 강종 public event Action? OnAllDisconnected; public DummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000) { sendInterval = sendIntervalMs; - clients = Enumerable.Range(0, count).Select(i => new DummyClients(i, ip, port, key)).ToList(); + clients = Enumerable.Range(1, count + 1).Select(i => new DummyClients(i, ip, port, key)).ToList(); Log.Information("[SERVICE] {Count}개 클라이언트 생성 → {Ip}:{Port}", count, ip, port); } public async Task RunAsync(CancellationToken ct) { - if (IsTest) - { - foreach (DummyClients c in clients) - { - c.TestCount = TestCount; - } - - Log.Information("[TEST] 유닛 테스트 모드: 클라이언트당 {Count}개 수신 시 자동 종료", TestCount); - } - - await Task.WhenAll( - PollLoopAsync(ct), - SendLoopAsync(ct) - ); + await Task.WhenAll(PollLoopAsync(ct), SendLoopAsync(ct)); } private async Task PollLoopAsync(CancellationToken ct) @@ -90,7 +64,7 @@ public class DummyClientService foreach (DummyClients client in clients) { - client.SendPing(); + client.SendTransform(); if (client.peer != null) { sent++; @@ -149,6 +123,7 @@ public class DummyClientService totalAvgRtt += c.AvgRttMs; rttClientCount++; } + if (c.peer != null) { connected++; diff --git a/ClientTester/EchoClientTester/DummyService/DummyClients.cs b/ClientTester/EchoClientTester/DummyService/DummyClients.cs new file mode 100644 index 0000000..0944d41 --- /dev/null +++ b/ClientTester/EchoClientTester/DummyService/DummyClients.cs @@ -0,0 +1,215 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using ClientTester.Packet; +using LiteNetLib; +using LiteNetLib.Utils; +using Serilog; + +namespace ClientTester.DummyService; + +public class DummyClients +{ + private NetManager manager; + private EventBasedNetListener listener; + private NetDataWriter? writer; + public NetPeer? peer; + public long clientId; + + // seq → 송신 타임스탬프 (Stopwatch tick) + private ConcurrentDictionary pendingPings = new(); + private int seqNumber; + private const int MAX_PENDING_PINGS = 1000; + + // info + private Vector3 position = new Vector3(); + private int rotY = 0; + private float moveSpeed = 3.5f; + private float distance = 0.0f; + private float preTime = 0.0f; + + // 이동 계산용 + private static readonly Random random = new(); + private readonly Stopwatch moveClock = Stopwatch.StartNew(); + private float posX = 0f; + private float posZ = 0f; + private float dirX = 0f; + private float dirZ = 0f; + + // 유닛 테스트용 (0 = 제한 없음) + public int TestCount + { + get; + set; + } = 0; + + // 통계 + 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(long clientId, string ip, int port, string key) + { + this.clientId = clientId; + listener = new EventBasedNetListener(); + manager = new NetManager(listener); + writer = new NetDataWriter(); + + listener.PeerConnectedEvent += netPeer => + { + peer = netPeer; + Log.Information("[Client {ClientId:00}] 연결됨", this.clientId); + + // clientID가 토큰의 hashKey라고 가정함 + PacketHeader packetHeader = new PacketHeader(); + packetHeader.Code = (PacketCode)1; + packetHeader.BodyLength = 4 + sizeof(long); + writer.Put(clientId); + peer.Send(writer, DeliveryMethod.ReliableOrdered); + + // 초기화 + }; + + listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) => + { + short code = reader.GetShort(); + short bodyLength = reader.GetShort(); + string? msg = reader.GetString(); + long sentTick; + + if (msg != null && msg.StartsWith("Echo seq:") && + int.TryParse(msg.Substring("Echo seq:".Length), out int seq) && + pendingPings.TryRemove(seq, out sentTick)) + { + double rttMs = (Stopwatch.GetTimestamp() - sentTick) * 1000.0 / Stopwatch.Frequency; + LastRttMs = rttMs; + TotalRttMs += rttMs; + RttCount++; + } + + ReceivedCount++; + + if (TestCount > 0 && ReceivedCount >= TestCount) + { + peer.Disconnect(); + } + + reader.Recycle(); + }; + + listener.PeerDisconnectedEvent += (peer, info) => + { + Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", this.clientId, info.Reason); + this.peer = null; + }; + + manager.Start(); + manager.Connect(ip, port, key); + } + + public void UpdateDummy() + { + // 델타 타임 계산 (초 단위) + float now = (float)moveClock.Elapsed.TotalSeconds; + float delta = preTime > 0f ? now - preTime : 0.1f; + preTime = now; + + // 남은 거리가 없으면 새 방향·목표 거리 설정 + if (distance <= 0f) + { + // 현재 각도에서 -30~+30도 범위로 회전 + rotY = (rotY + random.Next(-30, 31) + 360) % 360; + float rad = rotY * MathF.PI / 180f; + dirX = MathF.Sin(rad); + dirZ = MathF.Cos(rad); + + // 3초~12초에 도달할 수 있는 거리 = moveSpeed × 랜덤 초 + float seconds = 3f + (float)random.NextDouble() * 9f; + distance = moveSpeed * seconds; + } + + // 이번 틱 이동량 (남은 거리 초과 방지) + float step = MathF.Min(moveSpeed * delta, distance); + posX += dirX * step; + posZ += dirZ * step; + distance -= step; + + // 정수 Vector3 갱신 + position.X = (int)MathF.Round(posX); + position.Z = (int)MathF.Round(posZ); + } + + public void SendTransform() + { + if (peer == null || writer == null) + { + return; + } + + UpdateDummy(); + + int seq = seqNumber++; + pendingPings[seq] = Stopwatch.GetTimestamp(); + + // 응답 없는 오래된 ping 정리 (패킷 유실 시 메모리 누수 방지) + if (pendingPings.Count > MAX_PENDING_PINGS) + { + int cutoff = seq - MAX_PENDING_PINGS; + foreach (int key in pendingPings.Keys) + { + if (key < cutoff) + { + pendingPings.TryRemove(key, out _); + } + } + } + + PacketHeader packetHeader = new PacketHeader(); + packetHeader.Code = 0; + packetHeader.BodyLength = (ushort)$"Echo seq:{seq}".Length; + writer.Put((short)packetHeader.Code); + writer.Put((short)packetHeader.BodyLength); + writer.Put($"Echo seq:{seq}"); + // 순서보장 안함 HOL Blocking 제거 + peer.Send(writer, DeliveryMethod.ReliableUnordered); + SentCount++; + writer.Reset(); + } + + public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0; + + public void PollEvents() + { + manager.PollEvents(); + } + + public void Stop() + { + manager.Stop(); + } +} diff --git a/ClientTester/EchoClientTester/EchoDummyService/EchoDummyClientService.cs b/ClientTester/EchoClientTester/EchoDummyService/EchoDummyClientService.cs new file mode 100644 index 0000000..9bcbc03 --- /dev/null +++ b/ClientTester/EchoClientTester/EchoDummyService/EchoDummyClientService.cs @@ -0,0 +1,167 @@ +using LiteNetLib; +using Serilog; + +namespace ClientTester.EchoDummyService; + +public class EchoDummyClientService +{ + private readonly List clients; + private readonly int sendInterval; + + // 유닛 테스트용 (n패킷 시간체크) + public bool IsTest + { + get; + set; + } = false; + + public int TestCount + { + get; + set; + } = 100000; + + // 모든거 강종 + public event Action? OnAllDisconnected; + + public EchoDummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000) + { + sendInterval = sendIntervalMs; + clients = Enumerable.Range(0, count).Select(i => new EchoDummyClients(i, ip, port, key)).ToList(); + + Log.Information("[SERVICE] {Count}개 클라이언트 생성 → {Ip}:{Port}", count, ip, port); + } + + public async Task RunAsync(CancellationToken ct) + { + if (IsTest) + { + foreach (EchoDummyClients c in clients) + { + c.TestCount = TestCount; + } + + Log.Information("[TEST] 유닛 테스트 모드: 클라이언트당 {Count}개 수신 시 자동 종료", TestCount); + } + + await Task.WhenAll( + PollLoopAsync(ct), + SendLoopAsync(ct) + ); + } + + private async Task PollLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + foreach (EchoDummyClients c in clients) + { + c.PollEvents(); + } + + try + { + await Task.Delay(10, ct); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private async Task SendLoopAsync(CancellationToken ct) + { + int tick = 0; + + while (!ct.IsCancellationRequested) + { + int sent = 0; + int total = clients.Count; + + foreach (EchoDummyClients client in clients) + { + client.SendPing(); + if (client.peer != null) + { + sent++; + } + else + { + total--; + } + } + + if (total == 0) + { + Log.Information("All Disconnect Clients"); + OnAllDisconnected?.Invoke(); + break; + } + + Log.Debug("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, total); + tick++; + + try + { + await Task.Delay(sendInterval, ct); + } + catch (OperationCanceledException) + { + break; + } + } + } + + public void PrintStats() + { + int totalSent = 0, totalRecv = 0; + int connected = 0; + int rttClientCount = 0; + + Log.Information("───────────── Performance Report ─────────────"); + + double totalAvgRtt = 0; + + foreach (EchoDummyClients c in clients) + { + 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); + + totalSent += c.SentCount; + totalRecv += c.ReceivedCount; + if (c.RttCount > 0) + { + totalAvgRtt += c.AvgRttMs; + rttClientCount++; + } + if (c.peer != null) + { + connected++; + } + } + + double avgRtt = rttClientCount > 0 ? totalAvgRtt / rttClientCount : 0; + + Log.Information("────────────────────────────────────────────"); + Log.Information( + "[TOTAL] Sent={Sent} Recv={Recv} Connected={Connected}/{Total} AvgRTT={AvgRtt:F3}ms", + totalSent, totalRecv, connected, clients.Count, avgRtt); + Log.Information("────────────────────────────────────────────"); + } + + public void Stop() + { + foreach (EchoDummyClients c in clients) + { + c.Stop(); + } + + Log.Information("[SERVICE] 모든 클라이언트 종료됨."); + } +} diff --git a/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs b/ClientTester/EchoClientTester/EchoDummyService/EchoDummyClients.cs similarity index 95% rename from ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs rename to ClientTester/EchoClientTester/EchoDummyService/EchoDummyClients.cs index 2efb79a..95caa4b 100644 --- a/ClientTester/EchoClientTester/EchoDummyService/DummyClients.cs +++ b/ClientTester/EchoClientTester/EchoDummyService/EchoDummyClients.cs @@ -7,7 +7,7 @@ using Serilog; namespace ClientTester.EchoDummyService; -public class DummyClients +public class EchoDummyClients { private NetManager manager; private EventBasedNetListener listener; @@ -58,7 +58,7 @@ public class DummyClients get; } - public DummyClients(int clientId, string ip, int port, string key) + public EchoDummyClients(int clientId, string ip, int port, string key) { this.clientId = clientId; listener = new EventBasedNetListener(); @@ -131,7 +131,7 @@ public class DummyClients PacketHeader packetHeader = new PacketHeader(); packetHeader.Code = 0; - packetHeader.BodyLength = $"Echo seq:{seq}".Length; + packetHeader.BodyLength = (ushort)$"Echo seq:{seq}".Length; writer.Put((short)packetHeader.Code); writer.Put((short)packetHeader.BodyLength); writer.Put($"Echo seq:{seq}"); diff --git a/ClientTester/EchoClientTester/EchoDummyService/Map.cs b/ClientTester/EchoClientTester/EchoDummyService/Map.cs new file mode 100644 index 0000000..ebfd384 --- /dev/null +++ b/ClientTester/EchoClientTester/EchoDummyService/Map.cs @@ -0,0 +1,6 @@ +namespace ClientTester.EchoDummyService; + +public class Map +{ + // TODO : 맵 정보 필요 +} diff --git a/ClientTester/EchoClientTester/Packet/PacketBody.cs b/ClientTester/EchoClientTester/Packet/PacketBody.cs index e03683d..677c00f 100644 --- a/ClientTester/EchoClientTester/Packet/PacketBody.cs +++ b/ClientTester/EchoClientTester/Packet/PacketBody.cs @@ -100,24 +100,6 @@ public class PlayerInfo } } -[ProtoContract] -public class ItemInfo -{ - [ProtoMember(1)] - public int ItemId - { - get; - set; - } - - [ProtoMember(2)] - public int Count - { - get; - set; - } -} - // ============================================================ // 인증 // ============================================================ @@ -134,7 +116,7 @@ public class RecvTokenPacket } } -// LOAD_GAME +// LOAD_GAME 내 정보 [ProtoContract] public class LoadGamePacket { @@ -151,27 +133,96 @@ public class LoadGamePacket get; set; } + + [ProtoMember(3)] + public int MaplId + { + get; + set; + } } // ============================================================ // 로비 // ============================================================ -// INTO_LOBBY [ProtoContract] -public class IntoLobbyPacket +public class ChannelInfo { [ProtoMember(1)] + public int ChannelId + { + get; + set; + } + + [ProtoMember(2)] + public int ChannelUserConut + { + get; + set; + } + + [ProtoMember(3)] + public int ChannelUserMax + { + get; + set; + } +} + +[ProtoContract] +public class LoadChannelPacket +{ + [ProtoMember(1)] + public List Channels + { + get; + set; + } = new List(); +} + +// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 +[ProtoContract] +public class IntoChannelPacket +{ + [ProtoMember(1)] + public int ChannelId + { + get; + set; + } // 클라->서버: 입장할 채널 ID + + [ProtoMember(2)] public List Players { get; set; - } = null!; + } = new List(); // 서버->클라: 채널 내 플레이어 목록 } -// EXIT_LOBBY +// UPDATE_CHANNEL_USER 유저 접속/나감 [ProtoContract] -public class ExitLobbyPacket +public class UpdateChannelUserPacket +{ + [ProtoMember(1)] + public PlayerInfo Players + { + get; + set; + } + + [ProtoMember(2)] + public bool IsAdd + { + get; + set; + } +} + +// EXIT_CHANNEL 나가는 유저 +[ProtoContract] +public class ExitChannelPacket { [ProtoMember(1)] public int PlayerId @@ -181,174 +232,6 @@ public class ExitLobbyPacket } } -// ============================================================ -// 인스턴스 던전 -// ============================================================ - -public enum BossState -{ - START, - END, - PHASE_CHANGE -} - -public enum BossResult -{ - SUCCESS, - FAIL -} - -// INTO_INSTANCE -[ProtoContract] -public class IntoInstancePacket -{ - [ProtoMember(1)] - public int InstanceId - { - get; - set; - } - - [ProtoMember(2)] - public int BossId - { - get; - set; - } - - [ProtoMember(3)] - public List PlayerIds - { - get; - set; - } -} - -// UPDATE_BOSS -[ProtoContract] -public class UpdateBossPacket -{ - [ProtoMember(1)] - public BossState State - { - get; - set; - } - - [ProtoMember(2)] - public int Phase - { - get; - set; - } - - [ProtoMember(3)] - public BossResult Result - { - get; - set; - } // END일 때만 유효 -} - -// REWARD_INSTANCE -[ProtoContract] -public class RewardInstancePacket -{ - [ProtoMember(1)] - public int Exp - { - get; - set; - } - - [ProtoMember(2)] - public List Items - { - get; - set; - } -} - -// EXIT_INSTANCE -[ProtoContract] -public class ExitInstancePacket -{ - [ProtoMember(1)] - public int PlayerId - { - get; - set; - } -} - -// ============================================================ -// 파티 -// ============================================================ - -public enum PartyUpdateType -{ - CREATE, - DELETE -} - -public enum UserPartyUpdateType -{ - JOIN, - LEAVE -} - -// UPDATE_PARTY -[ProtoContract] -public class UpdatePartyPacket -{ - [ProtoMember(1)] - public int PartyId - { - get; - set; - } - - [ProtoMember(2)] - public PartyUpdateType Type - { - get; - set; - } - - [ProtoMember(3)] - public int LeaderId - { - get; - set; - } -} - -// UPDATE_USER_PARTY -[ProtoContract] -public class UpdateUserPartyPacket -{ - [ProtoMember(1)] - public int PartyId - { - get; - set; - } - - [ProtoMember(2)] - public int PlayerId - { - get; - set; - } - - [ProtoMember(3)] - public UserPartyUpdateType Type - { - get; - set; - } -} - // ============================================================ // 플레이어 // ============================================================ @@ -604,3 +487,71 @@ public class DamagePacket set; } } + +// ============================================================ +// 파티 +// ============================================================ + +public enum PartyUpdateType +{ + CREATE, + DELETE +} + +public enum UserPartyUpdateType +{ + JOIN, + LEAVE +} + +// UPDATE_PARTY +[ProtoContract] +public class UpdatePartyPacket +{ + [ProtoMember(1)] + public int PartyId + { + get; + set; + } + + [ProtoMember(2)] + public PartyUpdateType Type + { + get; + set; + } + + [ProtoMember(3)] + public int LeaderId + { + get; + set; + } +} + +// UPDATE_USER_PARTY +[ProtoContract] +public class UpdateUserPartyPacket +{ + [ProtoMember(1)] + public int PartyId + { + get; + set; + } + + [ProtoMember(2)] + public int PlayerId + { + get; + set; + } + + [ProtoMember(3)] + public UserPartyUpdateType Type + { + get; + set; + } +} diff --git a/ClientTester/EchoClientTester/Packet/PacketHeader.cs b/ClientTester/EchoClientTester/Packet/PacketHeader.cs index 3512a8b..fbe6ee6 100644 --- a/ClientTester/EchoClientTester/Packet/PacketHeader.cs +++ b/ClientTester/EchoClientTester/Packet/PacketHeader.cs @@ -1,61 +1,56 @@ namespace ClientTester.Packet; -public enum PacketCode : short +public enum PacketCode : ushort { - NONE, // 초기 클라이언트 시작시 jwt토큰 받아옴 RECV_TOKEN, - // jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송) + + // 내 정보 로드 (서버 -> 클라) LOAD_GAME, - // 마을(로비)진입시 모든 데이터 로드 - INTO_LOBBY, + // 모든 채널 로드 - jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송) + // (서버 -> 클라) + LOAD_CHANNEL, - // 로비 나가기 - EXIT_LOBBY, + // 나 채널 접속 (클라 -> 서버) + INTO_CHANNEL, - // 인스턴스 던전 입장 - INTO_INSTANCE, + // 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라) + UPDATE_CHANNEL_USER, - // 결과 보상 - REWARD_INSTANCE, + // 채널 나가기 (클라 -> 서버) + EXIT_CHANNEL, - // 보스전 (시작, 종료) - UPDATE_BOSS, + // 플레이어 위치, 방향 (서버 -> 클라 \ 클라 -> 서버) + TRANSFORM_PLAYER, - // 인스턴스 던전 퇴장 - EXIT_INSTANCE, + // 플레이어 행동 업데이트 (서버 -> 클라 \ 클라 -> 서버) + ACTION_PLAYER, + + // 플레이어 스테이트 업데이트 (서버 -> 클라 \ 클라 -> 서버) + STATE_PLAYER, + + // NPC 위치, 방향 (서버 -> 클라) + TRANSFORM_NPC, + + // NPC 행동 업데이트 (서버 -> 클라) + ACTION_NPC, + + // NPC 스테이트 업데이트 (서버 -> 클라) + STATE_NPC, + + // 데미지 UI 전달 (서버 -> 클라) + DAMAGE, // 파티 (생성, 삭제) UPDATE_PARTY, // 파티 유저 업데이트(추가 삭제) - UPDATE_USER_PARTY, - - // 플레이어 위치, 방향 - TRANSFORM_PLAYER, - - // 플레이어 행동 업데이트 - ACTION_PLAYER, - - // 플레이어 스테이트 업데이트 - STATE_PLAYER, - - // NPC 위치, 방향 - TRANSFORM_NPC, - - // NPC 행동 업데이트 - ACTION_NPC, - - // NPC 스테이트 업데이트 - STATE_NPC, - - // 데미지 UI 전달 - DAMAGE + UPDATE_USER_PARTY } public class PacketHeader { public PacketCode Code; - public int BodyLength; + public ushort BodyLength; } diff --git a/ClientTester/EchoClientTester/Packet/PacketSerializer.cs b/ClientTester/EchoClientTester/Packet/PacketSerializer.cs index 64a60b1..a9ed08c 100644 --- a/ClientTester/EchoClientTester/Packet/PacketSerializer.cs +++ b/ClientTester/EchoClientTester/Packet/PacketSerializer.cs @@ -16,10 +16,13 @@ namespace ClientTester.Packet ushort size = (ushort)payload.Length; byte[] result = new byte[4 + payload.Length]; + // 2바이트 패킷 타입 헤더 result[0] = (byte)(type & 0xFF); result[1] = (byte)(type >> 8); + // 2바이트 패킷 길이 헤더 result[2] = (byte)(size & 0xFF); result[3] = (byte)(size >> 8); + // protobuf 페이로드 Buffer.BlockCopy(payload, 0, result, 4, payload.Length); return result; } @@ -36,10 +39,12 @@ namespace ClientTester.Packet ushort type = (ushort)(data[0] | (data[1] << 8)); ushort size = (ushort)(data[2] | (data[3] << 8)); + // 헤더에 명시된 size와 실제 데이터 길이 검증 int actualPayloadLen = data.Length - 4; if (size > actualPayloadLen) { - Log.Warning("[PacketSerializer] 페이로드 크기 불일치 HeaderSize={Size} ActualSize={Actual}", size, actualPayloadLen); + Log.Warning("[PacketSerializer] 페이로드 크기 불일치 HeaderSize={Size} ActualSize={Actual}", size, + actualPayloadLen); return (0, 0, null)!; } diff --git a/ClientTester/EchoClientTester/Program.cs b/ClientTester/EchoClientTester/Program.cs index 1fa08a9..2aa011f 100644 --- a/ClientTester/EchoClientTester/Program.cs +++ b/ClientTester/EchoClientTester/Program.cs @@ -1,47 +1,116 @@ +using ClientTester.DummyService; using ClientTester.EchoDummyService; using Serilog; class EcoClientTester { - public static readonly string SERVER_IP = "tolelom.xyz"; + 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; + private async void StartEchoDummyTest() + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); + cts.Cancel(); + }; + + EchoDummyClientService service = + new EchoDummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); + service.OnAllDisconnected += () => + { + Log.Warning("[SHUTDOWN] 종료 이벤트 발생, 종료 중..."); + cts.Cancel(); + }; + + // service.IsTest = true; + // service.TestCount = 100; + await service.RunAsync(cts.Token); + + service.PrintStats(); + service.Stop(); + + await Log.CloseAndFlushAsync(); + } + catch (Exception e) + { + Log.Error($"[SHUTDOWN] 예외 발생 : {e}"); + } + } + + private async void StartDummyTest() + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); + cts.Cancel(); + }; + + DummyClientService service = + new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); + service.OnAllDisconnected += () => + { + Log.Warning("[SHUTDOWN] 종료 이벤트 발생, 종료 중..."); + cts.Cancel(); + }; + + await service.RunAsync(cts.Token); + + service.PrintStats(); + service.Stop(); + + await Log.CloseAndFlushAsync(); + } + catch (Exception e) + { + Log.Error($"[SHUTDOWN] 예외 발생 : {e}"); + } + } + private static async Task Main(string[] args) { // .MinimumLevel.Warning() // Warning 이상만 출력 배포시 - string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() - .WriteTo.File($"logs/log_{timestamp}.txt") + .WriteTo.File($"logs2/log_{timestamp}.txt") .CreateLogger(); - CancellationTokenSource cts = new CancellationTokenSource(); - Console.CancelKeyPress += (_, e) => + Log.Information("========== 더미 클라 테스터 =========="); + Log.Information("1. 에코 서버"); + Log.Information("2. 더미 클라(이동만)"); + Log.Information("===================================="); + Log.Information("1 / 2 : "); + + string? input = Console.ReadLine(); + if (!int.TryParse(input, out int choice) || (choice != 1 && choice != 2)) { - e.Cancel = true; - Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); - cts.Cancel(); - }; + Log.Warning("1 또는 2만 입력하세요."); + return; + } - DummyClientService service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); - service.OnAllDisconnected += () => + EcoClientTester tester = new EcoClientTester(); + if (choice == 1) { - Log.Warning("[SHUTDOWN] 종료 이벤트 발생, 종료 중..."); - cts.Cancel(); - }; - - // service.IsTest = true; - // service.TestCount = 100; - await service.RunAsync(cts.Token); - - service.PrintStats(); - service.Stop(); - - await Log.CloseAndFlushAsync(); + // 에코 서버 실행 + tester.StartEchoDummyTest(); + } + else if (choice == 2) + { + // 더미 클라 실행 + tester.StartDummyTest(); + } } } diff --git a/ClientTester/EchoClientTester/Vector3.cs b/ClientTester/EchoClientTester/Vector3.cs new file mode 100644 index 0000000..56b9853 --- /dev/null +++ b/ClientTester/EchoClientTester/Vector3.cs @@ -0,0 +1,8 @@ +namespace ClientTester; + +public class Vector3 +{ + public int X { get; set; } + public int Y { get; set; } + public int Z { get; set; } +}