Files
a301_mmo_game_server/ClientTester/EchoClientTester/StressTest/StressTestClient.cs

204 lines
6.4 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using ClientTester.DummyService;
using ClientTester.Packet;
using LiteNetLib;
using LiteNetLib.Utils;
using Serilog;
namespace ClientTester.StressTest;
/// <summary>
/// 스트레스 테스트용 클라이언트.
/// Echo RTT 측정 + 이동 패킷 전송을 동시에 수행.
/// </summary>
public class StressTestClient
{
private NetManager manager;
private EventBasedNetListener listener;
private NetDataWriter? writer;
public NetPeer? peer;
public int clientId;
// 이동
private Vector3 position = new Vector3();
private int rotY;
private float moveSpeed = 3.5f;
private float distance;
private float preTime;
private readonly Stopwatch moveClock = Stopwatch.StartNew();
private float posX, posZ, dirX, dirZ;
public MapBounds Map { get; set; } = new MapBounds(-50f, 50f, -50f, 50f);
// RTT 측정
private readonly ConcurrentDictionary<int, long> pendingPings = new();
private int seqNumber;
private const int MAX_PENDING = 500;
/// <summary>개별 RTT 기록 (퍼센타일 계산용)</summary>
public ConcurrentBag<double> RttSamples { get; } = new();
// 통계
public int SentCount { get; set; }
public int ReceivedCount { get; set; }
public int RttCount { get; set; }
public double TotalRttMs { get; set; }
public double LastRttMs { get; set; }
public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0;
public bool IsConnected => peer != null;
public StressTestClient(int 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.Debug("[Stress {Id:000}] 연결됨", clientId);
DummyAccTokenPacket token = new DummyAccTokenPacket { Token = clientId };
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.DUMMY_ACC_TOKEN, token);
writer!.Put(data);
peer.Send(writer, DeliveryMethod.ReliableOrdered);
writer.Reset();
};
listener.NetworkReceiveEvent += (p, reader, ch, dm) =>
{
ReceivedCount++;
try
{
byte[] raw = reader.GetRemainingBytes();
if (raw.Length >= 4)
{
ushort type = BitConverter.ToUInt16(raw, 0);
ushort size = BitConverter.ToUInt16(raw, 2);
if (type == (ushort)PacketCode.ECHO && size > 0 && raw.Length >= 4 + size)
{
byte[] payload = new byte[size];
Array.Copy(raw, 4, payload, 0, size);
EchoPacket echo = PacketSerializer.DeserializePayload<EchoPacket>(payload);
if (echo.Str.StartsWith("Echo seq:") &&
int.TryParse(echo.Str.AsSpan("Echo seq:".Length), out int seq) &&
pendingPings.TryRemove(seq, out long sentTick))
{
double rttMs = (Stopwatch.GetTimestamp() - sentTick) * 1000.0 / Stopwatch.Frequency;
LastRttMs = rttMs;
TotalRttMs += rttMs;
RttCount++;
RttSamples.Add(rttMs);
}
}
}
}
catch
{
// 파싱 실패는 무시 (다른 패킷 타입)
}
};
listener.PeerDisconnectedEvent += (p, info) =>
{
Log.Debug("[Stress {Id:000}] 끊김: {Reason}", clientId, info.Reason);
peer = null;
};
manager.Start();
manager.Connect(ip, port, key);
}
public void UpdateAndSendTransform()
{
if (peer == null || writer == null)
{
return;
}
// 이동 업데이트
float now = (float)moveClock.Elapsed.TotalSeconds;
float delta = preTime > 0f ? now - preTime : 0.1f;
preTime = now;
if (distance <= 0f)
{
int wallRotY = Map.GetRotYAwayFromWall(posX, posZ);
rotY = wallRotY >= 0
? (wallRotY + Random.Shared.Next(-30, 31) + 360) % 360
: (rotY + Random.Shared.Next(-30, 31) + 360) % 360;
float rad = rotY * MathF.PI / 180f;
dirX = MathF.Sin(rad);
dirZ = MathF.Cos(rad);
distance = moveSpeed * (3f + (float)Random.Shared.NextDouble() * 9f);
}
float step = MathF.Min(moveSpeed * delta, distance);
float nextX = posX + dirX * step;
float nextZ = posZ + dirZ * step;
bool hitWall = Map.Clamp(ref nextX, ref nextZ);
posX = nextX;
posZ = nextZ;
distance = hitWall ? 0f : distance - step;
position.X = (int)MathF.Round(posX);
position.Z = (int)MathF.Round(posZ);
// 전송
TransformPlayerPacket pkt = new TransformPlayerPacket
{
PlayerId = clientId,
RotY = rotY,
Position = new Packet.Vector3 { X = position.X, Y = 0, Z = position.Z }
};
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, pkt);
writer.Put(data);
peer.Send(writer, DeliveryMethod.Unreliable);
SentCount++;
writer.Reset();
}
public void SendPing()
{
if (peer == null || writer == null)
{
return;
}
int seq = seqNumber++;
pendingPings[seq] = Stopwatch.GetTimestamp();
if (pendingPings.Count > MAX_PENDING)
{
int cutoff = seq - MAX_PENDING;
foreach (int k in pendingPings.Keys)
{
if (k < cutoff)
{
pendingPings.TryRemove(k, out _);
}
}
}
EchoPacket echo = new EchoPacket { Str = $"Echo seq:{seq}" };
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ECHO, echo);
writer.Put(data);
peer.Send(writer, DeliveryMethod.ReliableUnordered);
SentCount++;
writer.Reset();
}
public void PollEvents()
{
manager.PollEvents();
}
public void Stop()
{
manager.Stop();
}
}