523 lines
15 KiB
C#
523 lines
15 KiB
C#
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;
|
|
}
|
|
}
|