feat : 더미 플레이어 구현 / 더미클라 서비스 에코용, 더미 플레이어용 분기
This commit is contained in:
151
ClientTester/EchoClientTester/DummyService/DummyClientService.cs
Normal file
151
ClientTester/EchoClientTester/DummyService/DummyClientService.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using LiteNetLib;
|
||||
using Serilog;
|
||||
|
||||
namespace ClientTester.DummyService;
|
||||
|
||||
public class DummyClientService
|
||||
{
|
||||
private readonly List<DummyClients> 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(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)
|
||||
{
|
||||
await Task.WhenAll(PollLoopAsync(ct), SendLoopAsync(ct));
|
||||
}
|
||||
|
||||
private async Task PollLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
foreach (DummyClients c in clients)
|
||||
{
|
||||
c.PollEvents();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(10, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendLoopAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(500, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int tick = 0;
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
int sent = 0;
|
||||
int total = clients.Count;
|
||||
|
||||
foreach (DummyClients client in clients)
|
||||
{
|
||||
client.SendTransform();
|
||||
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 (DummyClients 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 (DummyClients c in clients)
|
||||
{
|
||||
c.Stop();
|
||||
}
|
||||
|
||||
Log.Information("[SERVICE] 모든 클라이언트 종료됨.");
|
||||
}
|
||||
}
|
||||
215
ClientTester/EchoClientTester/DummyService/DummyClients.cs
Normal file
215
ClientTester/EchoClientTester/DummyService/DummyClients.cs
Normal file
@@ -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<int, long> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user