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 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(); 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; } }