2 Commits

5 changed files with 95 additions and 39 deletions

View File

@@ -11,29 +11,46 @@ public class RestApi : Singleton<RestApi>
private const string VERIFY_URL = "https://a301.api.tolelom.xyz/api/auth/verify";
private readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
// 토큰 검증 - 성공 시 username 반환, 실패(401/타임아웃 등) 시 null 반환
private const int MAX_RETRY = 3;
private static readonly TimeSpan RETRY_DELAY = TimeSpan.FromSeconds(1);
// 토큰 검증 - 성공 시 username 반환
// 401 → 재시도 없이 즉시 null 반환 (토큰 자체가 무효)
// 타임아웃/네트워크 오류 → 최대 MAX_RETRY회 재시도 후 null 반환
public async Task<string?> VerifyTokenAsync(string token)
{
try
for (int attempt = 1; attempt <= MAX_RETRY; attempt++)
{
HttpResponseMessage response = await httpClient.PostAsJsonAsync(VERIFY_URL, new { token });
if (response.StatusCode == HttpStatusCode.Unauthorized)
try
{
Log.Warning("[RestApi] 인증 실패 (401)");
return null;
HttpResponseMessage response = await httpClient.PostAsJsonAsync(VERIFY_URL, new { token });
// 401: 토큰 자체가 무효 → 재시도해도 같은 결과, 즉시 반환
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Log.Warning("[RestApi] 토큰 인증 실패 (401)");
return null;
}
response.EnsureSuccessStatusCode();
AuthVerifyResponse? result = await response.Content.ReadFromJsonAsync<AuthVerifyResponse>();
return result?.Username;
}
catch (Exception ex) when (attempt < MAX_RETRY)
{
// 일시적 장애 (타임아웃, 네트워크 오류 등) → 재시도
Log.Warning("[RestApi] 통신 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message);
await Task.Delay(RETRY_DELAY);
}
catch (Exception ex)
{
// 마지막 시도까지 실패
Log.Error("[RestApi] 최종 통신 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message);
}
response.EnsureSuccessStatusCode();
AuthVerifyResponse? result = await response.Content.ReadFromJsonAsync<AuthVerifyResponse>();
return result?.Username;
}
catch (Exception ex)
{
Log.Error("[RestApi] 웹서버 통신 실패: {Message}", ex.Message);
return null;
}
return null;
}
private sealed class AuthVerifyResponse

View File

@@ -85,7 +85,11 @@ public class GameServer : ServerBase
{
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
string token = accTokenPacket.Token;
long hashKey = UuidGeneratorManager.Instance.GetOrCreate(token);
tokenHash.TryGetValue(token, out long hashKey);
if (hashKey <= 1000)
{
hashKey = UuidGeneratorManager.Instance.Create();
}
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
{
@@ -102,6 +106,7 @@ public class GameServer : ServerBase
if (username == null)
{
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
UuidGeneratorManager.Instance.Release(hashKey);
peer.Disconnect();
return;
}
@@ -110,17 +115,11 @@ public class GameServer : ServerBase
}
peer.Tag = new Session(hashKey, peer);
((Session)peer.Tag).Token = token;
sessions[hashKey] = peer;
tokenHash[token] = hashKey;
pendingPeers.Remove(peer.Id);
if (hashKey <= 1000)
{
// 더미 클라이언트면 에러
Log.Error("[Server] Dummy 클라이언트가 입니다. 연결을 종료합니다. HashKey={Key} PeerId={Id}", hashKey, peer.Id);
peer.Disconnect();
return;
}
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
OnSessionConnected(peer, hashKey);
}
@@ -152,6 +151,7 @@ public class GameServer : ServerBase
{
Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason);
}
UuidGeneratorManager.Instance.Release(hashKey);
}
protected override void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload)

View File

@@ -1,21 +1,35 @@
using System.Collections.Concurrent;
namespace MMOserver.Utils;
public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
{
private long counter = 0;
private readonly ConcurrentDictionary<string, long> tokenMap = new();
// 0 ~ 1000 은 더미 클라이언트 예약 범위
private const long DUMMY_RANGE_MAX = 1000;
// string token → 고유 long ID 변환 (멀티스레드 안전)
// 동일 token은 항상 같은 ID 반환
public long GetOrCreate(string token)
private readonly object idLock = new();
private readonly HashSet<long> usedIds = new();
// 고유 랜덤 long ID 발급 (1001번 이상, 충돌 시 재생성)
public long Create()
{
return tokenMap.GetOrAdd(token, _ => Interlocked.Increment(ref counter));
lock (idLock)
{
long id;
do
{
id = Random.Shared.NextInt64(DUMMY_RANGE_MAX + 1, long.MaxValue);
} while (usedIds.Contains(id));
usedIds.Add(id);
return id;
}
}
public bool TryGet(string token, out long id)
// 로그아웃 / 세션 만료 시 ID 반납
public bool Release(long id)
{
return tokenMap.TryGetValue(token, out id);
lock (idLock)
{
return usedIds.Remove(id);
}
}
}

View File

@@ -36,6 +36,9 @@ public abstract class ServerBase : INetEventListener
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
protected readonly Dictionary<long, NetPeer> sessions = new();
// Token / HashKey 관리
protected readonly Dictionary<string, long> tokenHash = new();
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
private readonly NetDataWriter cachedWriter = new();
@@ -123,6 +126,12 @@ public abstract class ServerBase : INetEventListener
// (재연결로 이미 교체된 경우엔 건드리지 않음)
if (sessions.TryGetValue(session.HashKey, out NetPeer? current) && current.Id == peer.Id)
{
// 더미 클라 아니면 token관리
if (!string.IsNullOrEmpty(session.Token))
{
tokenHash.Remove(session.Token);
}
sessions.Remove(session.HashKey);
Log.Information("[Server] 세션 해제 HashKey={Key} Reason={Reason}", session.HashKey, disconnectInfo.Reason);
OnSessionDisconnected(peer, session.HashKey, disconnectInfo);

View File

@@ -4,12 +4,28 @@ namespace ServerLib.Service;
public class Session
{
public long HashKey { get; init; }
public NetPeer Peer { get; set; }
public string? Token
{
get;
set;
}
public long HashKey
{
get;
init;
}
public NetPeer Peer
{
get;
set;
}
public Session(long hashKey, NetPeer peer)
{
HashKey = hashKey;
Peer = peer;
Peer = peer;
Token = null;
}
}