204 lines
6.4 KiB
C#
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 = posX;
|
|
position.Z = posZ;
|
|
|
|
// 전송
|
|
TransformPlayerPacket pkt = new TransformPlayerPacket
|
|
{
|
|
PlayerId = clientId,
|
|
RotY = rotY,
|
|
Position = new Packet.Position { X = position.X, Y = -0.5f, 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();
|
|
}
|
|
}
|