- ARepository 전체 메서드 IDbConnection using 추가 (커넥션 풀 누수) - GameServer NotImplementedException → 로그 출력으로 변경 - ServerBase Auth/Echo payload 길이 검증 주석 해제 - PacketSerializer MemoryStream using 추가 (양쪽 솔루션) - PacketSerializer size 파라미터 제거, 자동 계산 + size 검증 구현 - ServerBase NetDataWriter cachedWriter 재사용 (GC 압력 감소) - ServerBase isListening volatile 추가 - ServerBase Deserialize 실패 시 null payload 체크 - ServerBase Serilog 구조적 로깅 템플릿 구문 수정 - TestHandler 전체 메서드 try-catch 추가 - TestRepository IDbConnectionFactory 인터페이스로 변경 - DummyClients BodyLength 계산 불일치 수정 - DummyClients pendingPings 메모리 누수 방지 - EchoClientTester async void 이벤트 → 동기 Cancel()로 변경 - ANALYSIS.md 코드 분석 및 문제점 보고서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
3.5 KiB
C#
143 lines
3.5 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using ClientTester.Packet;
|
|
using LiteNetLib;
|
|
using LiteNetLib.Utils;
|
|
using Serilog;
|
|
|
|
namespace ClientTester.EchoDummyService;
|
|
|
|
public class DummyClients
|
|
{
|
|
private NetManager manager;
|
|
private EventBasedNetListener listener;
|
|
private NetDataWriter? writer;
|
|
public NetPeer? peer;
|
|
public int clientId;
|
|
|
|
// seq → 송신 타임스탬프 (Stopwatch tick)
|
|
private ConcurrentDictionary<int, long> pendingPings = new();
|
|
private int seqNumber;
|
|
private const int MaxPendingPings = 1000;
|
|
|
|
// 통계
|
|
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(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++;
|
|
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 > MaxPendingPings)
|
|
{
|
|
int cutoff = seq - MaxPendingPings;
|
|
foreach (int key in pendingPings.Keys)
|
|
{
|
|
if (key < cutoff)
|
|
pendingPings.TryRemove(key, out _);
|
|
}
|
|
}
|
|
|
|
PacketHeader packetHeader = new PacketHeader();
|
|
packetHeader.Code = 0;
|
|
packetHeader.BodyLength = $"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();
|
|
}
|
|
}
|