feat : 에코 서버 / 클라이언트 기능 추가 작업

This commit is contained in:
qornwh1
2026-03-01 01:05:14 +09:00
parent 30457819b1
commit 97f6187f5d
13 changed files with 255 additions and 133 deletions

View File

@@ -1,17 +1,20 @@
using LiteNetLib.Utils; using LiteNetLib;
using Serilog; using Serilog;
namespace ClientTester.EchoDummyService; namespace ClientTester.EchoDummyService;
public class DummyClientService public class DummyClientService
{ {
private readonly List<DummyClients> _clients; private readonly List<DummyClients> clients;
private readonly int _sendInterval; private readonly int sendInterval;
// 모든거 강종
public event Action? OnAllDisconnected;
public DummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000) public DummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000)
{ {
_sendInterval = sendIntervalMs; sendInterval = sendIntervalMs;
_clients = Enumerable.Range(0, count) clients = Enumerable.Range(0, count)
.Select(i => new DummyClients(i, ip, port, key)) .Select(i => new DummyClients(i, ip, port, key))
.ToList(); .ToList();
@@ -30,62 +33,100 @@ public class DummyClientService
{ {
while (!ct.IsCancellationRequested) while (!ct.IsCancellationRequested)
{ {
foreach (var c in _clients) foreach (DummyClients c in clients)
{
c.PollEvents(); c.PollEvents();
}
try { await Task.Delay(15, ct); } try
catch (OperationCanceledException) { break; } {
await Task.Delay(15, ct);
}
catch (OperationCanceledException)
{
break;
}
} }
} }
private async Task SendLoopAsync(CancellationToken ct) private async Task SendLoopAsync(CancellationToken ct)
{ {
try { await Task.Delay(500, ct); } try
catch (OperationCanceledException) { return; } {
await Task.Delay(500, ct);
}
catch (OperationCanceledException)
{
return;
}
int tick = 0; int tick = 0;
while (!ct.IsCancellationRequested) while (!ct.IsCancellationRequested)
{ {
int sent = 0; int sent = 0;
int total = clients.Count;
foreach (var client in _clients) foreach (DummyClients client in clients)
{ {
client.SendPing(); client.SendPing();
if (client.Peer != null) sent++; if (client.peer != null)
{
sent++;
}
else
{
total--;
}
} }
Log.Information("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, _clients.Count); if (total == 0)
{
Log.Information("All Disconnect Clients");
OnAllDisconnected?.Invoke();
break;
}
Log.Information("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, total);
tick++; tick++;
try { await Task.Delay(_sendInterval, ct); } try
catch (OperationCanceledException) { break; } {
await Task.Delay(sendInterval, ct);
}
catch (OperationCanceledException)
{
break;
}
} }
} }
public void PrintStats() public void PrintStats()
{ {
int totalSent = 0, totalRecv = 0; int totalSent = 0, totalRecv = 0;
int connected = 0; int connected = 0;
Log.Information("════════════ Performance Report ════════════"); Log.Information("───────────── Performance Report ─────────────");
double totalAvgRtt = 0; double totalAvgRtt = 0;
foreach (var c in _clients) foreach (DummyClients c in clients)
{ {
var stats = c.Peer?.Statistics; NetStatistics? stats = c.peer?.Statistics;
long loss = stats?.PacketLoss ?? 0; long loss = stats?.PacketLoss ?? 0;
float lossPct = stats?.PacketLossPercent ?? 0f; float lossPct = stats?.PacketLossPercent ?? 0f;
Log.Information( Log.Information(
"[Client {ClientId:00}] Sent={Sent} Recv={Recv} | Loss={Loss}({LossPct:F1}%) AvgRTT={AvgRtt:F3}ms LastRTT={LastRtt:F3}ms", "[Client {ClientId:00}] Sent={Sent} Recv={Recv} | Loss={Loss}({LossPct:F1}%) AvgRTT={AvgRtt:F3}ms LastRTT={LastRtt:F3}ms",
c.ClientId, c.SentCount, c.ReceivedCount, loss, lossPct, c.AvgRttMs, c.LastRttMs); c.clientId, c.SentCount, c.ReceivedCount, loss, lossPct, c.AvgRttMs, c.LastRttMs);
totalSent += c.SentCount; totalSent += c.SentCount;
totalRecv += c.ReceivedCount; totalRecv += c.ReceivedCount;
totalAvgRtt += c.AvgRttMs; totalAvgRtt += c.AvgRttMs;
if (c.Peer != null) connected++; if (c.peer != null)
{
connected++;
}
} }
double avgRtt = connected > 0 ? totalAvgRtt / connected : 0; double avgRtt = connected > 0 ? totalAvgRtt / connected : 0;
@@ -93,14 +134,16 @@ public class DummyClientService
Log.Information("────────────────────────────────────────────"); Log.Information("────────────────────────────────────────────");
Log.Information( Log.Information(
"[TOTAL] Sent={Sent} Recv={Recv} Connected={Connected}/{Total} AvgRTT={AvgRtt:F3}ms", "[TOTAL] Sent={Sent} Recv={Recv} Connected={Connected}/{Total} AvgRTT={AvgRtt:F3}ms",
totalSent, totalRecv, connected, _clients.Count, avgRtt); totalSent, totalRecv, connected, clients.Count, avgRtt);
Log.Information("════════════════════════════════════════════"); Log.Information("────────────────────────────────────────────");
} }
public void Stop() public void Stop()
{ {
foreach (var c in _clients) foreach (DummyClients c in clients)
{
c.Stop(); c.Stop();
}
Log.Information("[SERVICE] 모든 클라이언트 종료됨."); Log.Information("[SERVICE] 모든 클라이언트 종료됨.");
} }

View File

@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using ClientTester.Packet;
using LiteNetLib; using LiteNetLib;
using LiteNetLib.Utils; using LiteNetLib.Utils;
using Serilog; using Serilog;
@@ -7,82 +8,112 @@ namespace ClientTester.EchoDummyService;
public class DummyClients public class DummyClients
{ {
public NetManager Manager; public NetManager manager;
public EventBasedNetListener Listener; public EventBasedNetListener listener;
public NetPeer? Peer; public NetPeer? peer;
public int ClientId; public int clientId;
// seq → 송신 타임스탬프 (Stopwatch tick) // seq → 송신 타임스탬프 (Stopwatch tick)
private readonly Dictionary<int, long> _pendingPings = new(); private readonly Dictionary<int, long> pendingPings = new();
private int _seqNumber; private int seqNumber;
// 통계 // 통계
public int SentCount; public int SentCount
public int ReceivedCount; {
public double LastRttMs; set;
public double TotalRttMs; get;
public int RttCount; }
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) public DummyClients(int clientId, string ip, int port, string key)
{ {
ClientId = clientId; this.clientId = clientId;
Listener = new EventBasedNetListener(); listener = new EventBasedNetListener();
Manager = new NetManager(Listener); manager = new NetManager(listener);
Listener.PeerConnectedEvent += peer => listener.PeerConnectedEvent += netPeer =>
{ {
Peer = peer; peer = netPeer;
Log.Information("[Client {ClientId:00}] 연결됨", ClientId); Log.Information("[Client {ClientId:00}] 연결됨", this.clientId);
}; };
Listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) => listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) =>
{ {
var msg = reader.GetString(); string? msg = reader.GetString();
// "ack:seq:{seqNum}" 파싱 string[] parts = msg.Split(':');
var parts = msg.Split(':'); if (parts.Length == 3 && parts[0] == "ack" && parts[1] == "seq" && int.TryParse(parts[2], out int seq) && pendingPings.TryGetValue(seq, out long sentTick))
if (parts.Length == 3 && parts[0] == "ack" && parts[1] == "seq"
&& int.TryParse(parts[2], out int seq)
&& _pendingPings.TryGetValue(seq, out long sentTick))
{ {
double rttMs = (Stopwatch.GetTimestamp() - sentTick) double rttMs = (Stopwatch.GetTimestamp() - sentTick) * 1000.0 / Stopwatch.Frequency;
* 1000.0 / Stopwatch.Frequency;
LastRttMs = rttMs; LastRttMs = rttMs;
TotalRttMs += rttMs; TotalRttMs += rttMs;
RttCount++; RttCount++;
_pendingPings.Remove(seq); pendingPings.Remove(seq);
} }
ReceivedCount++; ReceivedCount++;
reader.Recycle(); reader.Recycle();
}; };
Listener.PeerDisconnectedEvent += (peer, info) => listener.PeerDisconnectedEvent += (peer, info) =>
{ {
Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", ClientId, info.Reason); Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", this.clientId, info.Reason);
Peer = null; this.peer = null;
}; };
Manager.Start(); manager.Start();
Manager.Connect(ip, port, key); manager.Connect(ip, port, key);
} }
public void SendPing() public void SendPing()
{ {
if (Peer is null) return; if (peer == null)
{
return;
}
int seq = _seqNumber++; int seq = seqNumber++;
_pendingPings[seq] = Stopwatch.GetTimestamp(); pendingPings[seq] = Stopwatch.GetTimestamp();
var writer = new NetDataWriter(); NetDataWriter writer = new NetDataWriter();
writer.Put($"seq:{seq}"); PacketHeader packetHeader = new PacketHeader();
Peer.Send(writer, DeliveryMethod.ReliableOrdered); packetHeader.Code = 0;
packetHeader.BodyLength = $"seq:{seq}".Length;
writer.Put((short)packetHeader.Code);
writer.Put((short)packetHeader.BodyLength);
writer.Put($"Echo seq:{seq}");
peer.Send(writer, DeliveryMethod.ReliableOrdered);
SentCount++; SentCount++;
} }
public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0; public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0;
public void PollEvents() => Manager.PollEvents(); public void PollEvents()
{
manager.PollEvents();
}
public void Stop() => Manager.Stop(); public void Stop()
{
manager.Stop();
}
} }

View File

@@ -1,3 +1,5 @@
#pragma warning disable CS8618
using ProtoBuf; using ProtoBuf;
namespace ClientTester.Packet; namespace ClientTester.Packet;
@@ -164,7 +166,7 @@ public class IntoLobbyPacket
{ {
get; get;
set; set;
} } = null!;
} }
// EXIT_LOBBY // EXIT_LOBBY

View File

@@ -1,6 +1,6 @@
namespace ClientTester.Packet; namespace ClientTester.Packet;
public enum PacketCode public enum PacketCode : short
{ {
NONE, NONE,
// 초기 클라이언트 시작시 jwt토큰 받아옴 // 초기 클라이언트 시작시 jwt토큰 받아옴

View File

@@ -1,28 +1,37 @@
using ClientTester.EchoDummyService; using ClientTester.EchoDummyService;
using Serilog; using Serilog;
Log.Logger = new LoggerConfiguration() class EcoClientTester
.WriteTo.Console()
.CreateLogger();
const string SERVER_IP = "localhost";
const int SERVER_PORT = 9500;
const string CONNECTION_KEY = "game";
const int CLIENT_COUNT = 1000;
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{ {
e.Cancel = true; public static readonly string SERVER_IP = "localhost";
Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중..."); public static readonly int SERVER_PORT = 9500;
cts.Cancel(); public static readonly string CONNECTION_KEY = "test";
}; public static readonly int CLIENT_COUNT = 100;
var service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100); private static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
await service.RunAsync(cts.Token); CancellationTokenSource cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중...");
cts.Cancel();
};
service.PrintStats(); DummyClientService service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100);
service.Stop(); service.OnAllDisconnected += async () =>
{
Log.Warning("[SHUTDOWN] 종료 이벤트 발생, 종료 중...");
await cts.CancelAsync();
};
Log.CloseAndFlush(); await service.RunAsync(cts.Token);
service.PrintStats();
service.Stop();
await Log.CloseAndFlushAsync();
}
}

View File

@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<LangVersion>13</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,6 +1,8 @@
#pragma warning disable CS8618
using ProtoBuf; using ProtoBuf;
namespace ClientTester.Packet; namespace MMOserver.Packet;
// ============================================================ // ============================================================
// 공통 타입 // 공통 타입

View File

@@ -1,6 +1,6 @@
namespace ClientTester.Packet; namespace MMOserver.Packet;
public enum PacketCode public enum PacketCode : short
{ {
NONE, NONE,
// 초기 클라이언트 시작시 jwt토큰 받아옴 // 초기 클라이언트 시작시 jwt토큰 받아옴

View File

@@ -1,6 +1,8 @@
using MMOserver.Game; using MMOserver.Game;
using Serilog; using Serilog;
namespace MMOserver;
class Program class Program
{ {
private static void Main() private static void Main()

View File

@@ -1,3 +1,5 @@
using System;
using System.IO;
using ProtoBuf; using ProtoBuf;
using Serilog; using Serilog;

View File

@@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using LiteNetLib; using LiteNetLib;
using LiteNetLib.Utils; using LiteNetLib.Utils;
using Serilog; using Serilog;
@@ -122,6 +125,13 @@ public abstract class ServerBase : INetEventListener
{ {
(ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data); (ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data);
// 0이라면 에코 서버 테스트용 따로 처리
if (type == 0)
{
HandleEcho(peer, payload);
return;
}
// Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요) // Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요)
if (type == (ushort)PacketType.Auth) if (type == (ushort)PacketType.Auth)
{ {
@@ -167,16 +177,33 @@ public abstract class ServerBase : INetEventListener
} }
} }
// Echo 서버 테스트
private void HandleEcho(NetPeer peer, byte[] payload)
{
// if (payload.Length < sizeof(long))
// {
// Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id}", peer.Id);
// peer.Disconnect();
// return;
// }
// 세션에 넣지는 않는다.
NetDataReader reader = new NetDataReader(payload);
Log.Debug("[Echo] : addr={Addr}, str={Str}", peer.Address, reader.GetString());
SendTo(peer, payload);
}
// ─── Auth 처리 (내부) ──────────────────────────────────────────────── // ─── Auth 처리 (내부) ────────────────────────────────────────────────
private void HandleAuth(NetPeer peer, byte[] payload) private void HandleAuth(NetPeer peer, byte[] payload)
{ {
if (payload.Length < sizeof(long)) // if (payload.Length < sizeof(long))
{ // {
Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id); // Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id);
peer.Disconnect(); // peer.Disconnect();
return; // return;
} // }
long hashKey = BitConverter.ToInt64(payload, 0); long hashKey = BitConverter.ToInt64(payload, 0);

View File

@@ -1,4 +1,6 @@
namespace ServerLib.Service; using System.Collections.Generic;
namespace ServerLib.Service;
public class SessionManager public class SessionManager
{ {