Compare commits
2 Commits
18fd8a0737
...
bfa3394ad1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfa3394ad1 | ||
|
|
c8ce36a624 |
@@ -11,29 +11,46 @@ public class RestApi : Singleton<RestApi>
|
|||||||
private const string VERIFY_URL = "https://a301.api.tolelom.xyz/api/auth/verify";
|
private const string VERIFY_URL = "https://a301.api.tolelom.xyz/api/auth/verify";
|
||||||
private readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
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)
|
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 });
|
try
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
{
|
||||||
Log.Warning("[RestApi] 인증 실패 (401)");
|
HttpResponseMessage response = await httpClient.PostAsJsonAsync(VERIFY_URL, new { token });
|
||||||
return null;
|
|
||||||
|
// 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
|
private sealed class AuthVerifyResponse
|
||||||
|
|||||||
@@ -85,7 +85,11 @@ public class GameServer : ServerBase
|
|||||||
{
|
{
|
||||||
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
||||||
string token = accTokenPacket.Token;
|
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))
|
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
|
||||||
{
|
{
|
||||||
@@ -102,6 +106,7 @@ public class GameServer : ServerBase
|
|||||||
if (username == null)
|
if (username == null)
|
||||||
{
|
{
|
||||||
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
|
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
|
||||||
|
UuidGeneratorManager.Instance.Release(hashKey);
|
||||||
peer.Disconnect();
|
peer.Disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,17 +115,11 @@ public class GameServer : ServerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
peer.Tag = new Session(hashKey, peer);
|
peer.Tag = new Session(hashKey, peer);
|
||||||
|
((Session)peer.Tag).Token = token;
|
||||||
sessions[hashKey] = peer;
|
sessions[hashKey] = peer;
|
||||||
|
tokenHash[token] = hashKey;
|
||||||
pendingPeers.Remove(peer.Id);
|
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);
|
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
||||||
OnSessionConnected(peer, hashKey);
|
OnSessionConnected(peer, hashKey);
|
||||||
}
|
}
|
||||||
@@ -152,6 +151,7 @@ public class GameServer : ServerBase
|
|||||||
{
|
{
|
||||||
Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason);
|
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)
|
protected override void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload)
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace MMOserver.Utils;
|
namespace MMOserver.Utils;
|
||||||
|
|
||||||
public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
|
public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
|
||||||
{
|
{
|
||||||
private long counter = 0;
|
// 0 ~ 1000 은 더미 클라이언트 예약 범위
|
||||||
private readonly ConcurrentDictionary<string, long> tokenMap = new();
|
private const long DUMMY_RANGE_MAX = 1000;
|
||||||
|
|
||||||
// string token → 고유 long ID 변환 (멀티스레드 안전)
|
private readonly object idLock = new();
|
||||||
// 동일 token은 항상 같은 ID 반환
|
private readonly HashSet<long> usedIds = new();
|
||||||
public long GetOrCreate(string token)
|
|
||||||
|
// 고유 랜덤 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public abstract class ServerBase : INetEventListener
|
|||||||
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
|
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
|
||||||
protected readonly Dictionary<long, NetPeer> sessions = new();
|
protected readonly Dictionary<long, NetPeer> sessions = new();
|
||||||
|
|
||||||
|
// Token / HashKey 관리
|
||||||
|
protected readonly Dictionary<string, long> tokenHash = new();
|
||||||
|
|
||||||
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
|
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
|
||||||
private readonly NetDataWriter cachedWriter = new();
|
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)
|
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);
|
sessions.Remove(session.HashKey);
|
||||||
Log.Information("[Server] 세션 해제 HashKey={Key} Reason={Reason}", session.HashKey, disconnectInfo.Reason);
|
Log.Information("[Server] 세션 해제 HashKey={Key} Reason={Reason}", session.HashKey, disconnectInfo.Reason);
|
||||||
OnSessionDisconnected(peer, session.HashKey, disconnectInfo);
|
OnSessionDisconnected(peer, session.HashKey, disconnectInfo);
|
||||||
|
|||||||
@@ -4,12 +4,28 @@ namespace ServerLib.Service;
|
|||||||
|
|
||||||
public class Session
|
public class Session
|
||||||
{
|
{
|
||||||
public long HashKey { get; init; }
|
public string? Token
|
||||||
public NetPeer Peer { get; set; }
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long HashKey
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
init;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetPeer Peer
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
public Session(long hashKey, NetPeer peer)
|
public Session(long hashKey, NetPeer peer)
|
||||||
{
|
{
|
||||||
HashKey = hashKey;
|
HashKey = hashKey;
|
||||||
Peer = peer;
|
Peer = peer;
|
||||||
|
Token = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user