초기커밋

This commit is contained in:
2025-05-01 07:20:41 +09:00
commit 98bb2e3c5c
2747 changed files with 646947 additions and 0 deletions

View File

@@ -0,0 +1,522 @@
using System;
using System.Threading.Tasks;
using Nettention.Proud;
using ServerCore;
namespace GameServer;
//===================================================================
// 호스트 마이그레이션 정책 - 기획서 기반
//===================================================================
public class HostMigrationPolicy
{
// 현재 호스트의 Ping이 너무 높을 경우, 호스트를 교체(마이그레이션)하기 위한 임계값(ms).
public int HostKickPingThreshold { get; set; } = 500;
// PingKickThreshold를 초과한 상태가 몇 초(sec) 동안 지속되면 호스트 마이그레이션이 일어나는 지를 설정. 도중에 정상 복귀하면 타이머 리셋.
public TimeSpan HostKickPingDuration { get; set; } = TimeSpan.FromSeconds(30);
// 호스트의 FPS가 이 값 이하면 마이그레이션 타이머 발동.
public int HostKickFpsThreshold { get; set; } = 30;
// HostFpsThreshold를 초과한 상태가 몇 초(sec) 동안 지속되면 호스트 마이그레이션이 일어나는 지를 설정. 도중에 정상 복귀하면 타이머 리셋.
public TimeSpan HostKickFpsDuration { get; set; } = TimeSpan.FromSeconds(30);
// 한 번 호스트 마이그레이션이 발생한 후, 다시 마이그레이션이 일어나기까지의 최소 대기 시간(쿨다운, min)으로 아예 응답이 없는 경우 쿨다운을 무시하고 즉시 호스트 마이그레이션 발동.
public TimeSpan MigrationCooldown { get; set; } = TimeSpan.FromSeconds(60);
// 입장 후 호스트 변경 유예 시간 ms
public long ExcludeNewJoineeDurationTimeMs { get; set; } = 0;
}
//===================================================================
// 호스트 마이그레이션 정책을 적용해서 호스트를 마이그레이션을 진행하는 클래스
//===================================================================
public class HostMigrationSystem
{
public enum MigrationStateType
{
None,
Ping,
Fps
}
public class MigrationState
{
public enum CheckType
{
None,
Greater,
Less
}
private readonly CheckType m_check_type;
private readonly int m_policy_threshold_value = 0;
private readonly TimeSpan m_policy_duration;
public MigrationState(MigrationStateType migrationStateType, CheckType checkType, int thresholdValue,
TimeSpan duration)
{
m_check_type = checkType;
m_policy_threshold_value = thresholdValue;
m_policy_duration = duration;
StateType = migrationStateType;
reset();
}
public MigrationStateType StateType { get; set; }
private DateTime m_trigger_time = DateTime.MinValue;
// public StateType State { get; set; } = StateType.None;
private int m_sum;
private int m_sum_count;
public void reset()
{
m_trigger_time = DateTime.MinValue;
m_sum = 0;
m_sum_count = 0;
}
public bool isHostKicked()
{
if (m_sum_count == 0)
{
return false;
}
var value = (int)Math.Round((double)m_sum / m_sum_count);
return shouldTrigger(value);
}
public bool isHostKickDurationExceeded()
{
return isHostKickCheckTriggered() && DateTime.UtcNow > m_trigger_time.Add(m_policy_duration);
}
public void setTriggerTime(DateTime? triggerTime = null)
{
if (triggerTime == null)
{
return;
}
if (m_trigger_time == DateTime.MinValue)
{
m_trigger_time = DateTime.UtcNow;
}
}
public void update(int value)
{
if (shouldTrigger(value))
{
setTriggerTime();
}
// TriggerTime이 설정된 경우에만 Sum을 계산
if (isHostKickCheckTriggered())
{
m_sum += value;
m_sum_count++;
}
}
private bool isHostKickCheckTriggered()
{
return m_trigger_time != DateTime.MinValue;
}
private bool shouldTrigger(int value)
{
return m_check_type switch
{
CheckType.Greater => value > m_policy_threshold_value,
CheckType.Less => value < m_policy_threshold_value,
_ => false
};
}
}
private readonly HostMigrationPolicy m_host_migration_policy;
private readonly NetServer m_server;
private HostID m_group_host_id = HostID.HostID_None;
private HostID m_super_peer_host_id = HostID.HostID_None;
private readonly List<MigrationState> m_migration_states;
private DateTime m_last_migration_time = DateTime.MinValue;
private bool m_ignore_migration_condition;
public HostMigrationPolicy HostMigrationPolicy => m_host_migration_policy;
public HostID SuperPeerHostId => m_super_peer_host_id;
public HostID GroupHostHostId => m_group_host_id;
public NetClientInfo? CurrentSuperPeerInfo => m_server.GetClientInfo(m_super_peer_host_id);
// StartServerParameter.enablePingTest = true 옵션이 필요함
public HostMigrationSystem(NetServer server, HostMigrationPolicy hostMigrationPolicy)
{
m_server = server;
m_host_migration_policy = hostMigrationPolicy;
m_migration_states =
[
new MigrationState(MigrationStateType.Fps, MigrationState.CheckType.Less,
m_host_migration_policy.HostKickFpsThreshold,
m_host_migration_policy.HostKickFpsDuration),
new MigrationState(MigrationStateType.Ping, MigrationState.CheckType.Greater,
m_host_migration_policy.HostKickPingThreshold,
m_host_migration_policy.HostKickPingDuration)
];
var proudnet_policy = SuperPeerSelectionPolicy.GetOrdinary();
// 클라이언트가 로딩하는 시간동안 프레임 측정이 정화하지 않을 수 있기 때문에 오차가 발생할 수 있음
// 게임 시작 기준에 따라 설정이나 migrate() 호출 시점을 변경해야할 수 있음
proudnet_policy.m_excludeNewJoineeDurationTimeMs = hostMigrationPolicy.ExcludeNewJoineeDurationTimeMs;
}
//=================================================================
// P2P 그룹 아이디 설정 - 최초 한번만 설정됨
//=================================================================
public void setGroupHostId(HostID groupHostId)
{
if (m_group_host_id == HostID.HostID_None && groupHostId != HostID.HostID_None)
{
m_group_host_id = groupHostId;
}
}
//=================================================================
// 현재 수퍼 피어 아이디 설정
// - getMostSuitableSuperPeer() 호출 후
// - 클라이언트에 호스트 변경 요청
// - 클라이언트 응답을 받은 후, changeSuperPeerId() 호출
//=================================================================
public bool changeSuperPeerHostId(HostID superPeerId)
{
if (!isHostIdInP2PGroup(superPeerId))
{
return false;
}
m_super_peer_host_id = superPeerId;
return true;
}
//=================================================================
// 호스트 변경 요청
// migrate() 호출 시점에 따라 마이그레이션이 발생하지 않을 수 있음
// migrate()가 처리되지 않아도 에러가 아니기 때문에 bool을 반환함
//=================================================================
public (bool, HostID) migrateCheck()
{
if (m_ignore_migration_condition)
{
return migrateCheckIgnoreCondition();
}
try
{
// P2P 그룹이 설정돼 있는 지 확인
if (!isValidGroup())
{
return (false, m_super_peer_host_id);
}
// 최초에 수퍼 피어가 설정되지 않은 경우
var super_peer_id = m_super_peer_host_id;
if (m_super_peer_host_id == HostID.HostID_None)
{
super_peer_id = m_server.GetMostSuitableSuperPeerInGroup(m_group_host_id);
changeSuperPeerHostId(super_peer_id);
return (true, super_peer_id);
}
if (!isValidSuperPeer())
{
return (false, m_super_peer_host_id);
}
// 설정 시간만큼 설정 시간 유지
if (isOnMigrationCooldown())
{
return (false, m_super_peer_host_id);
}
hostMigrationStateUpdate();
if (!isHostKickDurationExceeded())
{
return (false, m_super_peer_host_id);
}
// 마이그레이션이 발생할 수 있는 조건인지 체크
if (isHostKicked())
{
super_peer_id = findSuperPeer();
}
resetMigrationState();
return (true, super_peer_id);
}
catch(Exception e)
{
Log.getLogger().error($"HostMigrationSystem.migrateCheck => {e}");
}
return (false, m_super_peer_host_id);
}
private void resetMigrationState()
{
foreach (var state in m_migration_states)
{
state.reset();
}
m_last_migration_time = DateTime.MinValue;
}
//=================================================================
// 호스트 변경 요청 - 무조건
//=================================================================
public (bool, HostID) getMostSuitableSuperPeerIgnoreCondition()
{
// P2P 그룹이 설정돼 있는 지 확인
if (!isValidGroup())
{
return (false, m_super_peer_host_id);
}
// checkAndValidateSuperPeer();
// if (isValidSuperPeer())
// {
// // 설정 시간만큼 설정 시간 유지
// if (isOnMigrationCooldown())
// {
// return (false, m_super_peer_id);
// }
//
// // 마이그레이션이 발생할 수 있는 조건인지 체크
// if (!isHostMigrateCondition())
// {
// return (false, m_super_peer_id);
// }
// }
//
// resetMigrationState();
var super_peer_id = m_server.GetMostSuitableSuperPeerInGroup(m_group_host_id);
if (super_peer_id == m_super_peer_host_id)
{
return (false, m_super_peer_host_id);
}
m_last_migration_time = DateTime.UtcNow;
return (true, super_peer_id);
}
//=================================================================
// 호스트 변경 요청
// getMostSuitableSuperPeerAsync() 호출 시점에 따라 마이그레이션이 발생하지 않을 수 있음
// getMostSuitableSuperPeerAsync()가 처리되지 않아도 에러가 아니기 때문에 bool을 반환함
//=================================================================
public async Task<(bool, HostID)> migrateCheckAsync()
{
return await Task.Run(migrateCheck);
}
private (bool, HostID) migrateCheckIgnoreCondition()
{
try
{
// P2P 그룹이 설정돼 있는 지 확인
if (!isValidGroup())
{
return (false, m_super_peer_host_id);
}
var super_peer_id = findSuperPeer();
resetMigrationState();
return (true, super_peer_id);
}
catch(Exception e)
{
Log.getLogger().error($"HostMigrationSystem.migrateCheckIgnoreCondition => {e}");
}
finally
{
m_ignore_migration_condition = false;
}
return (true, m_super_peer_host_id);
}
private void hostMigrationStateUpdate()
{
var info = m_server.GetClientInfo(m_super_peer_host_id);
m_migration_states.ForEach(state =>
{
switch (state.StateType)
{
case MigrationStateType.Fps:
state.update((int)info.recentFrameRate);
break;
case MigrationStateType.Ping:
state.update(info.recentPingMs);
break;
}
});
}
private bool isHostKicked()
{
return m_migration_states.Any(state => state.isHostKicked());
}
private bool isHostKickDurationExceeded()
{
return m_migration_states.Any(state => state.isHostKickDurationExceeded());
}
//=================================================================
// 호스트 변경 유예 조건 체크
//=================================================================
private bool isOnMigrationCooldown()
{
// 설정 시간 만큼 유지
return m_last_migration_time.Add(m_host_migration_policy.MigrationCooldown) > DateTime.UtcNow;
}
//=================================================================
// 호스트 그룹이 삭제됐는 지 확인
//=================================================================
private bool isValidGroup()
{
// 호스트 그룹 아이디가 설정되지 않은 경우
if (m_group_host_id == HostID.HostID_None)
{
return false;
}
// 호스트 그룹 아이디가 설정되어 있지만, P2P 그룹 정보가 없는 경우
if (m_group_host_id != HostID.HostID_None && m_server.GetP2PGroupInfo(m_group_host_id) == null)
{
m_group_host_id = HostID.HostID_None;
return false;
}
// 그룹에 속한 호스트가 없는 경우
if (m_group_host_id != HostID.HostID_None && m_server.GetP2PGroupInfo(m_group_host_id).members.GetCount() == 0)
{
m_group_host_id = HostID.HostID_None;
return false;
}
return true;
}
//=================================================================
// 수퍼 피어가 설정되어 있는지 체크
//=================================================================
private bool isValidSuperPeer()
{
// 수퍼 피어가 퇴장한 경우 등을 체크
if (m_super_peer_host_id != HostID.HostID_None && m_server.GetClientInfo(m_super_peer_host_id) == null)
{
m_super_peer_host_id = HostID.HostID_None;
}
return m_super_peer_host_id != HostID.HostID_None;
}
//=================================================================
// HostID가 p2pGroup에 속해 있는지 체크
//=================================================================
private bool isHostIdInP2PGroup(HostID hostId)
{
if (!isValidGroup())
{
return false;
}
return Enumerable.Range(0, m_server.GetP2PGroupInfo(m_group_host_id).members.GetCount())
.Any(i => m_server.GetP2PGroupInfo(m_group_host_id).members.At(i) == hostId);
}
private HostID findSuperPeer()
{
var peer_infos = new SortedList<HostID, NetClientInfo>();
var group_info = m_server.GetP2PGroupInfo(m_group_host_id);
Enumerable.Range(0, group_info.members.GetCount()).ToList().ForEach(i =>
{
var host_id = group_info.members.At(i);
if (host_id == m_super_peer_host_id)
{
return;
}
var info = m_server.GetClientInfo(host_id);
if (info == null)
{
return;
}
peer_infos.Add(info.hostID, info);
});
var recommend_super_peer_id = m_server.GetMostSuitableSuperPeerInGroup(m_group_host_id);
// peer_infos.TryGetValue(recommend_super_peer_id, out var recommend_super_peer_info);
var recommend_super_peer_info = m_server.GetClientInfo(recommend_super_peer_id);
NullReferenceCheckHelper.throwIfNull(recommend_super_peer_info, () => "recommend_super_peer_info is null");
if (!isValidPolicy(recommend_super_peer_info))
{
// 추천된 수퍼 피어가 마이그레이션 조건을 만족하지 않는 경우
// 커스텀 추천 로직 구현
// recentPingMs > HostKickPingThreshold && recentFrameRate < HostKickFpsThreshold
var custom_super_peer_info = peer_infos
.OrderBy(x => x.Value.recentPingMs)
.FirstOrDefault(x => isValidPolicy(x.Value));
if (custom_super_peer_info.Value != null)
{
Log.getLogger().info($"Host Migration Custom - recommend_super_peer_id: {recommend_super_peer_id}");
recommend_super_peer_id = custom_super_peer_info.Key;
}
else
{
// 정책에 맞는 수퍼 피어가 없는 경우 ping이 가장 낮은 수퍼 피어를 추천
var second_super_peer_info = peer_infos.OrderBy(x => x.Value.recentPingMs).FirstOrDefault();
if (second_super_peer_info.Value != null)
{
Log.getLogger().info($"Host Migration Second - recommend_super_peer_id: {recommend_super_peer_id}");
recommend_super_peer_id = second_super_peer_info.Key;
}
}
}
m_last_migration_time = DateTime.UtcNow;
Log.getLogger().info($"Host Migration ProudNet - recommend_super_peer_id: {recommend_super_peer_id}");
return recommend_super_peer_id;
bool isValidPolicy(NetClientInfo info)
{
return info.recentFrameRate > m_host_migration_policy.HostKickFpsThreshold &&
info.recentPingMs < m_host_migration_policy.HostKickPingThreshold;
}
}
//=================================================================
// 호스트 마이그레이션 조건을 무시할 지 설정 - 이슈 방지를 위해서 1회성으로 제안함
//=================================================================
public void setIgnoreMigrationCondition(bool ignore)
{
m_ignore_migration_condition = ignore;
}
}