using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using ClientTester.Packet; using LiteNetLib; using LiteNetLib.Utils; using Serilog; namespace ClientTester.EchoDummyService; public class EchoDummyClients { private NetManager manager; private EventBasedNetListener listener; private NetDataWriter? writer; public NetPeer? peer; public int clientId; // seq → 송신 타임스탬프 (Stopwatch tick) private ConcurrentDictionary pendingPings = new(); private int seqNumber; private const int MAX_PENDING_PINGS = 1000; // 유닛 테스트용 (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 EchoDummyClients(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.Information("[Client {ClientId:00}] 연결됨", this.clientId); }; 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 SendPing() { if (peer == null || writer == null) { return; } 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 _); } } } // string → raw bytes (길이 prefix 방지) EchoPacket echoPacket = new EchoPacket(); echoPacket.Str = $"Echo seq:{seq}"; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ECHO, echoPacket); writer.Put(data); // 순서보장 안함 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(); } }