using System.Collections.Concurrent; using System.Diagnostics; using ClientTester.DummyService; using ClientTester.Packet; using LiteNetLib; using LiteNetLib.Utils; using Serilog; namespace ClientTester.StressTest; /// /// 스트레스 테스트용 클라이언트. /// Echo RTT 측정 + 이동 패킷 전송을 동시에 수행. /// 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 pendingPings = new(); private int seqNumber; private const int MAX_PENDING = 500; /// 개별 RTT 기록 (퍼센타일 계산용) public ConcurrentBag 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(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(); } }