fix : 코드 분석 후 버그 및 결함 16건 수정
- 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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using LiteNetLib;
|
||||
using Serilog;
|
||||
using ServerLib.Service;
|
||||
|
||||
namespace MMOserver.Game;
|
||||
@@ -12,16 +13,16 @@ public class GameServer : ServerBase
|
||||
|
||||
protected override void OnSessionConnected(NetPeer peer, long hashKey)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Log.Information("[GameServer] 세션 연결 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
||||
}
|
||||
|
||||
protected override void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason);
|
||||
}
|
||||
|
||||
protected override void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Log.Debug("[GameServer] 패킷 수신 HashKey={Key} Type={Type} Size={Size}", hashKey, type, payload.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,37 +12,79 @@ public class TestHandler
|
||||
|
||||
public async Task<string> GetTestAsync(int id)
|
||||
{
|
||||
Test? result = await _testService.GetTestAsync(id);
|
||||
return result is null
|
||||
? HandlerHelper.Error("Test not found")
|
||||
: HandlerHelper.Success(result);
|
||||
try
|
||||
{
|
||||
Test? result = await _testService.GetTestAsync(id);
|
||||
return result is null
|
||||
? HandlerHelper.Error("Test not found")
|
||||
: HandlerHelper.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetTestByUuidAsync(string uuid)
|
||||
{
|
||||
Test? result = await _testService.GetTestByUuidAsync(uuid);
|
||||
return result is null
|
||||
? HandlerHelper.Error("Test not found")
|
||||
: HandlerHelper.Success(result);
|
||||
try
|
||||
{
|
||||
Test? result = await _testService.GetTestByUuidAsync(uuid);
|
||||
return result is null
|
||||
? HandlerHelper.Error("Test not found")
|
||||
: HandlerHelper.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetAllTestsAsync()
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.GetAllTestsAsync());
|
||||
try
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.GetAllTestsAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreateTestAsync(int testA, string testB, double testC)
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.CreateTestAsync(testA, testB, testC));
|
||||
try
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.CreateTestAsync(testA, testB, testC));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> UpdateTestAsync(int id, int? testA, string? testB, double? testC)
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.UpdateTestAsync(id, testA, testB, testC));
|
||||
try
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.UpdateTestAsync(id, testA, testB, testC));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> DeleteTestAsync(int id)
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.DeleteTestAsync(id));
|
||||
try
|
||||
{
|
||||
return HandlerHelper.Success(await _testService.DeleteTestAsync(id));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandlerHelper.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace MMOserver.RDB.Repositories;
|
||||
// 실제 호출할 쿼리들
|
||||
public class TestRepository : ARepository<Test>
|
||||
{
|
||||
public TestRepository(DbConnectionFactory factory) : base(factory)
|
||||
public TestRepository(IDbConnectionFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -9,22 +9,24 @@ namespace ServerLib.Packet
|
||||
|
||||
public static class PacketSerializer
|
||||
{
|
||||
// 직렬화: 객체 → byte[]
|
||||
public static byte[] Serialize<T>(ushort type, ushort size, T packet)
|
||||
// 직렬화: 객체 → byte[] (size는 payload 크기로 자동 계산)
|
||||
public static byte[] Serialize<T>(ushort type, T packet)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream();
|
||||
using MemoryStream payloadMs = new MemoryStream();
|
||||
Serializer.Serialize(payloadMs, packet);
|
||||
byte[] payload = payloadMs.ToArray();
|
||||
ushort size = (ushort)payload.Length;
|
||||
|
||||
byte[] result = new byte[4 + payload.Length];
|
||||
// 2바이트 패킷 타입 헤더
|
||||
ms.WriteByte((byte)(type & 0xFF));
|
||||
ms.WriteByte((byte)(type >> 8));
|
||||
|
||||
result[0] = (byte)(type & 0xFF);
|
||||
result[1] = (byte)(type >> 8);
|
||||
// 2바이트 패킷 길이 헤더
|
||||
ms.WriteByte((byte)(size & 0xFF));
|
||||
ms.WriteByte((byte)(size >> 8));
|
||||
|
||||
result[2] = (byte)(size & 0xFF);
|
||||
result[3] = (byte)(size >> 8);
|
||||
// protobuf 페이로드
|
||||
Serializer.Serialize(ms, packet);
|
||||
return ms.ToArray();
|
||||
Buffer.BlockCopy(payload, 0, result, 4, payload.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 역직렬화: byte[] → (PacketType, payload bytes)
|
||||
@@ -36,19 +38,26 @@ namespace ServerLib.Packet
|
||||
return (0, 0, null)!;
|
||||
}
|
||||
|
||||
// 길이체크도 필요함
|
||||
|
||||
ushort type = (ushort)(data[0] | (data[1] << 8));
|
||||
ushort size = (ushort)(data[2] | (data[3] << 8));
|
||||
byte[] payload = new byte[data.Length - 4];
|
||||
Buffer.BlockCopy(data, 4, payload, 0, payload.Length);
|
||||
|
||||
// 헤더에 명시된 size와 실제 데이터 길이 검증
|
||||
int actualPayloadLen = data.Length - 4;
|
||||
if (size > actualPayloadLen)
|
||||
{
|
||||
Log.Warning("[PacketSerializer] 페이로드 크기 불일치 HeaderSize={Size} ActualSize={Actual}", size, actualPayloadLen);
|
||||
return (0, 0, null)!;
|
||||
}
|
||||
|
||||
byte[] payload = new byte[size];
|
||||
Buffer.BlockCopy(data, 4, payload, 0, size);
|
||||
return (type, size, payload);
|
||||
}
|
||||
|
||||
// 페이로드 → 특정 타입으로 역직렬화
|
||||
public static T DeserializePayload<T>(byte[] payload)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream(payload);
|
||||
using MemoryStream ms = new MemoryStream(payload);
|
||||
return Serializer.Deserialize<T>(ms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,59 +22,59 @@ public abstract class ARepository<T> where T : class
|
||||
// Dapper.Contrib 기본 CRUD
|
||||
public async Task<T?> GetByIdAsync(int id)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.GetAsync<T>(id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> GetAllAsync()
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.GetAllAsync<T>();
|
||||
}
|
||||
|
||||
public async Task<long> InsertAsync(T entity)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.InsertAsync(entity);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(T entity)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.UpdateAsync(entity);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(T entity)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
// 커스텀 쿼리 헬퍼 (복잡한 쿼리용)
|
||||
protected async Task<IEnumerable<T>> QueryAsync(string sql, object? param = null)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.QueryAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
protected async Task<T?> QueryFirstOrDefaultAsync(string sql, object? param = null)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.QueryFirstOrDefaultAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
protected async Task<int> ExecuteAsync(string sql, object? param = null)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
return await conn.ExecuteAsync(sql, param);
|
||||
}
|
||||
|
||||
// 트랜잭션 헬퍼
|
||||
protected async Task<TResult> WithTransactionAsync<TResult>(Func<IDbConnection, IDbTransaction, Task<TResult>> action)
|
||||
{
|
||||
IDbConnection conn = CreateConnection();
|
||||
using IDbConnection conn = CreateConnection();
|
||||
conn.Open();
|
||||
IDbTransaction tx = conn.BeginTransaction();
|
||||
using IDbTransaction tx = conn.BeginTransaction();
|
||||
try
|
||||
{
|
||||
TResult result = await action(conn, tx);
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class ServerBase : INetEventListener
|
||||
public int Port { get; }
|
||||
public string ConnectionString { get; }
|
||||
|
||||
private bool isListening = false;
|
||||
private volatile bool isListening = false;
|
||||
|
||||
public ServerBase(int port, string connectionString)
|
||||
{
|
||||
@@ -86,7 +86,7 @@ public abstract class ServerBase : INetEventListener
|
||||
// 벤 기능 추가? 한국 ip만?
|
||||
if (request.AcceptIfKey(ConnectionString) == null)
|
||||
{
|
||||
Log.Debug("해당 클라이언트의 ConnectionKey={request.ConnectionKey}가 동일하지 않습니다", request.Data.ToString());
|
||||
Log.Debug("해당 클라이언트의 ConnectionKey가 동일하지 않습니다. Data={Data}", request.Data.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,13 @@ public abstract class ServerBase : INetEventListener
|
||||
{
|
||||
(ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data);
|
||||
|
||||
// Deserialize 실패 시 payload가 null
|
||||
if (payload == null)
|
||||
{
|
||||
Log.Warning("[Server] 패킷 역직렬화 실패 PeerId={Id} DataLen={Len}", peer.Id, data.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
// 0이라면 에코 서버 테스트용 따로 처리
|
||||
if (type == 0)
|
||||
{
|
||||
@@ -181,12 +188,11 @@ public abstract class ServerBase : INetEventListener
|
||||
|
||||
private void HandleEcho(NetPeer peer, byte[] payload)
|
||||
{
|
||||
// if (payload.Length < sizeof(long))
|
||||
// {
|
||||
// Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id}", peer.Id);
|
||||
// peer.Disconnect();
|
||||
// return;
|
||||
// }
|
||||
if (payload.Length < 4)
|
||||
{
|
||||
Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id} Length={Len}", peer.Id, payload.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
// 세션에 넣지는 않는다.
|
||||
NetDataReader reader = new NetDataReader(payload);
|
||||
@@ -201,12 +207,12 @@ public abstract class ServerBase : INetEventListener
|
||||
|
||||
private void HandleAuth(NetPeer peer, byte[] payload)
|
||||
{
|
||||
// if (payload.Length < sizeof(long))
|
||||
// {
|
||||
// Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id);
|
||||
// peer.Disconnect();
|
||||
// return;
|
||||
// }
|
||||
if (payload.Length < sizeof(long))
|
||||
{
|
||||
Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id} Length={Len}", peer.Id, payload.Length);
|
||||
peer.Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
long hashKey = BitConverter.ToInt64(payload, 0);
|
||||
|
||||
@@ -229,31 +235,31 @@ public abstract class ServerBase : INetEventListener
|
||||
|
||||
// ─── 전송 헬퍼 ───────────────────────────────────────────────────────
|
||||
|
||||
// NetDataWriter writer 풀처리 필요할듯
|
||||
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
|
||||
private readonly NetDataWriter cachedWriter = new();
|
||||
|
||||
// peer에게 전송
|
||||
protected void SendTo(NetPeer peer, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||
{
|
||||
NetDataWriter writer = new NetDataWriter();
|
||||
writer.Put(data);
|
||||
peer.Send(writer, method);
|
||||
cachedWriter.Reset();
|
||||
cachedWriter.Put(data);
|
||||
peer.Send(cachedWriter, method);
|
||||
}
|
||||
|
||||
// 모두에게 전송
|
||||
protected void Broadcast(byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||
{
|
||||
// 일단 channelNumber는 건드리지 않는다
|
||||
NetDataWriter writer = new NetDataWriter();
|
||||
writer.Put(data);
|
||||
netManager.SendToAll(writer, 0, method);
|
||||
cachedWriter.Reset();
|
||||
cachedWriter.Put(data);
|
||||
netManager.SendToAll(cachedWriter, 0, method);
|
||||
}
|
||||
|
||||
// exclude 1개 제외 / 나 제외 정도
|
||||
protected void BroadcastExcept(byte[] data, NetPeer exclude, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||
{
|
||||
// 일단 channelNumber는 건드리지 않는다
|
||||
NetDataWriter writer = new NetDataWriter();
|
||||
writer.Put(data);
|
||||
netManager.SendToAll(writer, 0, method, exclude);
|
||||
cachedWriter.Reset();
|
||||
cachedWriter.Put(data);
|
||||
netManager.SendToAll(cachedWriter, 0, method, exclude);
|
||||
}
|
||||
|
||||
// ─── 서브클래스 구현 ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user