Seven is the number.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

610 lines
20 KiB

4 years ago
  1. // ----------------------------------------------------------------------------
  2. // <copyright file="RegionHandler.cs" company="Exit Games GmbH">
  3. // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
  4. // </copyright>
  5. // <summary>
  6. // The RegionHandler class provides methods to ping a list of regions,
  7. // to find the one with best ping.
  8. // </summary>
  9. // <author>developer@photonengine.com</author>
  10. // ----------------------------------------------------------------------------
  11. #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
  12. #define SUPPORTED_UNITY
  13. #endif
  14. #if UNITY_WEBGL || UNITY_SWITCH
  15. #define PING_VIA_COROUTINE
  16. #endif
  17. namespace Photon.Realtime
  18. {
  19. using System;
  20. using System.Text;
  21. using System.Net;
  22. using System.Collections;
  23. using System.Collections.Generic;
  24. using System.Diagnostics;
  25. using ExitGames.Client.Photon;
  26. #if SUPPORTED_UNITY
  27. using UnityEngine;
  28. using Debug = UnityEngine.Debug;
  29. #endif
  30. #if SUPPORTED_UNITY || NETFX_CORE
  31. using Hashtable = ExitGames.Client.Photon.Hashtable;
  32. using SupportClass = ExitGames.Client.Photon.SupportClass;
  33. #endif
  34. /// <summary>
  35. /// Provides methods to work with Photon's regions (Photon Cloud) and can be use to find the one with best ping.
  36. /// </summary>
  37. /// <remarks>
  38. /// When a client uses a Name Server to fetch the list of available regions, the LoadBalancingClient will create a RegionHandler
  39. /// and provide it via the OnRegionListReceived callback.
  40. ///
  41. /// Your logic can decide to either connect to one of those regional servers, or it may use PingMinimumOfRegions to test
  42. /// which region provides the best ping.
  43. ///
  44. /// It makes sense to make clients "sticky" to a region when one gets selected.
  45. /// This can be achieved by storing the SummaryToCache value, once pinging was done.
  46. /// When the client connects again, the previous SummaryToCache helps limiting the number of regions to ping.
  47. /// In best case, only the previously selected region gets re-pinged and if the current ping is not much worse, this one region is used again.
  48. /// </remarks>
  49. public class RegionHandler
  50. {
  51. /// <summary>A list of region names for the Photon Cloud. Set by the result of OpGetRegions().</summary>
  52. /// <remarks>
  53. /// Implement ILoadBalancingCallbacks and register for the callbacks to get OnRegionListReceived(RegionHandler regionHandler).
  54. /// You can also put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.
  55. /// </remarks>
  56. public List<Region> EnabledRegions { get; protected internal set; }
  57. private string availableRegionCodes;
  58. private Region bestRegionCache;
  59. /// <summary>
  60. /// When PingMinimumOfRegions was called and completed, the BestRegion is identified by best ping.
  61. /// </summary>
  62. public Region BestRegion
  63. {
  64. get
  65. {
  66. if (this.EnabledRegions == null)
  67. {
  68. return null;
  69. }
  70. if (this.bestRegionCache != null)
  71. {
  72. return this.bestRegionCache;
  73. }
  74. this.EnabledRegions.Sort((a, b) => { return (a.Ping == b.Ping) ? 0 : (a.Ping < b.Ping) ? -1 : 1; });
  75. this.bestRegionCache = this.EnabledRegions[0];
  76. return this.bestRegionCache;
  77. }
  78. }
  79. /// <summary>
  80. /// This value summarizes the results of pinging currently available regions (after PingMinimumOfRegions finished).
  81. /// </summary>
  82. /// <remarks>
  83. /// This value should be stored in the client by the game logic.
  84. /// When connecting again, use it as previous summary to speed up pinging regions and to make the best region sticky for the client.
  85. /// </remarks>
  86. public string SummaryToCache
  87. {
  88. get
  89. {
  90. if (this.BestRegion != null) {
  91. return this.BestRegion.Code + ";" + this.BestRegion.Ping + ";" + this.availableRegionCodes;
  92. }
  93. return this.availableRegionCodes;
  94. }
  95. }
  96. #if PING_VIA_COROUTINE
  97. private ConnectionHandler connectionHandler;
  98. public RegionHandler()
  99. {
  100. this.connectionHandler = UnityEngine.Object.FindObjectOfType<ConnectionHandler>();
  101. if (!connectionHandler)
  102. {
  103. Debug.LogError("ConnectionHandler component not found. It is required to start regions ping coroutine.");
  104. }
  105. }
  106. #endif
  107. public string GetResults()
  108. {
  109. StringBuilder sb = new StringBuilder();
  110. sb.AppendFormat("Region Pinging Result: {0}\n", this.BestRegion.ToString());
  111. if (this.pingerList != null)
  112. {
  113. foreach (RegionPinger region in this.pingerList)
  114. {
  115. sb.AppendFormat(region.GetResults() + "\n");
  116. }
  117. }
  118. sb.AppendFormat("Previous summary: {0}", this.previousSummaryProvided);
  119. return sb.ToString();
  120. }
  121. public void SetRegions(OperationResponse opGetRegions)
  122. {
  123. if (opGetRegions.OperationCode != OperationCode.GetRegions)
  124. {
  125. return;
  126. }
  127. if (opGetRegions.ReturnCode != ErrorCode.Ok)
  128. {
  129. return;
  130. }
  131. string[] regions = opGetRegions[ParameterCode.Region] as string[];
  132. string[] servers = opGetRegions[ParameterCode.Address] as string[];
  133. if (regions == null || servers == null || regions.Length != servers.Length)
  134. {
  135. //TODO: log error
  136. //Debug.LogError("The region arrays from Name Server are not ok. Must be non-null and same length. " + (regions == null) + " " + (servers == null) + "\n" + opGetRegions.ToStringFull());
  137. return;
  138. }
  139. this.bestRegionCache = null;
  140. this.EnabledRegions = new List<Region>(regions.Length);
  141. for (int i = 0; i < regions.Length; i++)
  142. {
  143. Region tmp = new Region(regions[i], servers[i]);
  144. if (string.IsNullOrEmpty(tmp.Code))
  145. {
  146. continue;
  147. }
  148. this.EnabledRegions.Add(tmp);
  149. }
  150. Array.Sort(regions);
  151. this.availableRegionCodes = string.Join(",", regions);
  152. }
  153. private List<RegionPinger> pingerList;
  154. private Action<RegionHandler> onCompleteCall;
  155. private int previousPing;
  156. public bool IsPinging { get; private set; }
  157. private string previousSummaryProvided;
  158. public bool PingMinimumOfRegions(Action<RegionHandler> onCompleteCallback, string previousSummary)
  159. {
  160. if (this.EnabledRegions == null || this.EnabledRegions.Count == 0)
  161. {
  162. //TODO: log error
  163. //Debug.LogError("No regions available. Maybe all got filtered out or the AppId is not correctly configured.");
  164. return false;
  165. }
  166. if (this.IsPinging)
  167. {
  168. //TODO: log warning
  169. //Debug.LogWarning("PingMinimumOfRegions() skipped, because this RegionHander is already pinging some regions.");
  170. return false;
  171. }
  172. this.IsPinging = true;
  173. this.onCompleteCall = onCompleteCallback;
  174. this.previousSummaryProvided = previousSummary;
  175. if (string.IsNullOrEmpty(previousSummary))
  176. {
  177. return this.PingEnabledRegions();
  178. }
  179. string[] values = previousSummary.Split(';');
  180. if (values.Length < 3)
  181. {
  182. return this.PingEnabledRegions();
  183. }
  184. int prevBestRegionPing;
  185. bool secondValueIsInt = Int32.TryParse(values[1], out prevBestRegionPing);
  186. if (!secondValueIsInt)
  187. {
  188. return this.PingEnabledRegions();
  189. }
  190. string prevBestRegionCode = values[0];
  191. string prevAvailableRegionCodes = values[2];
  192. if (string.IsNullOrEmpty(prevBestRegionCode))
  193. {
  194. return this.PingEnabledRegions();
  195. }
  196. if (string.IsNullOrEmpty(prevAvailableRegionCodes))
  197. {
  198. return this.PingEnabledRegions();
  199. }
  200. if (!this.availableRegionCodes.Equals(prevAvailableRegionCodes) || !this.availableRegionCodes.Contains(prevBestRegionCode))
  201. {
  202. return this.PingEnabledRegions();
  203. }
  204. if (prevBestRegionPing >= RegionPinger.PingWhenFailed)
  205. {
  206. return this.PingEnabledRegions();
  207. }
  208. // let's check only the preferred region to detect if it's still "good enough"
  209. this.previousPing = prevBestRegionPing;
  210. Region preferred = this.EnabledRegions.Find(r => r.Code.Equals(prevBestRegionCode));
  211. RegionPinger singlePinger = new RegionPinger(preferred, this.OnPreferredRegionPinged);
  212. #if PING_VIA_COROUTINE
  213. singlePinger.Start(this.connectionHandler);
  214. #else
  215. singlePinger.Start();
  216. #endif
  217. return true;
  218. }
  219. private void OnPreferredRegionPinged(Region preferredRegion)
  220. {
  221. if (preferredRegion.Ping > this.previousPing * 1.50f)
  222. {
  223. this.PingEnabledRegions();
  224. }
  225. else
  226. {
  227. this.IsPinging = false;
  228. this.onCompleteCall(this);
  229. }
  230. }
  231. private bool PingEnabledRegions()
  232. {
  233. if (this.EnabledRegions == null || this.EnabledRegions.Count == 0)
  234. {
  235. //TODO: log
  236. //Debug.LogError("No regions available. Maybe all got filtered out or the AppId is not correctly configured.");
  237. return false;
  238. }
  239. this.pingerList = new List<RegionPinger>();
  240. foreach (Region region in this.EnabledRegions)
  241. {
  242. RegionPinger rp = new RegionPinger(region, this.OnRegionDone);
  243. this.pingerList.Add(rp);
  244. #if PING_VIA_COROUTINE
  245. rp.Start(this.connectionHandler); // TODO: check return value
  246. #else
  247. rp.Start(); // TODO: check return value
  248. #endif
  249. }
  250. return true;
  251. }
  252. private void OnRegionDone(Region region)
  253. {
  254. this.bestRegionCache = null;
  255. foreach (RegionPinger pinger in this.pingerList)
  256. {
  257. if (!pinger.Done)
  258. {
  259. return;
  260. }
  261. }
  262. this.IsPinging = false;
  263. this.onCompleteCall(this);
  264. }
  265. }
  266. public class RegionPinger
  267. {
  268. public static int Attempts = 5;
  269. public static bool IgnoreInitialAttempt = true;
  270. public static int MaxMilliseconsPerPing = 800; // enter a value you're sure some server can beat (have a lower rtt)
  271. public static int PingWhenFailed = Attempts * MaxMilliseconsPerPing;
  272. private Region region;
  273. private string regionAddress;
  274. public int CurrentAttempt = 0;
  275. public bool Done { get; private set; }
  276. private Action<Region> onDoneCall;
  277. private PhotonPing ping;
  278. private List<int> rttResults;
  279. public RegionPinger(Region region, Action<Region> onDoneCallback)
  280. {
  281. this.region = region;
  282. this.region.Ping = PingWhenFailed;
  283. this.Done = false;
  284. this.onDoneCall = onDoneCallback;
  285. }
  286. private PhotonPing GetPingImplementation()
  287. {
  288. PhotonPing ping = null;
  289. #if !NETFX_CORE
  290. if (LoadBalancingPeer.PingImplementation == typeof(PingMono))
  291. {
  292. ping = new PingMono(); // using this type explicitly saves it from IL2CPP bytecode stripping
  293. }
  294. #endif
  295. #if NATIVE_SOCKETS
  296. if (LoadBalancingPeer.PingImplementation == typeof(PingNativeDynamic))
  297. {
  298. ping = new PingNativeDynamic();
  299. }
  300. #endif
  301. #if UNITY_WEBGL
  302. if (LoadBalancingPeer.PingImplementation == typeof(PingHttp))
  303. {
  304. ping = new PingHttp();
  305. }
  306. #endif
  307. if (ping == null)
  308. {
  309. ping = (PhotonPing)Activator.CreateInstance(LoadBalancingPeer.PingImplementation);
  310. }
  311. return ping;
  312. }
  313. #if PING_VIA_COROUTINE
  314. public bool Start(ConnectionHandler connectionHandler)
  315. #else
  316. public bool Start()
  317. #endif
  318. {
  319. // all addresses for Photon region servers will contain a :port ending. this needs to be removed first.
  320. // PhotonPing.StartPing() requires a plain (IP) address without port or protocol-prefix (on all but Windows 8.1 and WebGL platforms).
  321. string address = this.region.HostAndPort;
  322. int indexOfColon = address.LastIndexOf(':');
  323. if (indexOfColon > 1)
  324. {
  325. address = address.Substring(0, indexOfColon);
  326. }
  327. this.regionAddress = ResolveHost(address);
  328. this.ping = this.GetPingImplementation();
  329. this.Done = false;
  330. this.CurrentAttempt = 0;
  331. this.rttResults = new List<int>(Attempts);
  332. #if PING_VIA_COROUTINE
  333. if (connectionHandler)
  334. {
  335. connectionHandler.StartCoroutine(this.RegionPingCoroutine());
  336. }
  337. else
  338. {
  339. Debug.LogError("ConnectionHandler component is null or destroyed. It is required to start regions ping coroutine.");
  340. }
  341. #elif UNITY_SWITCH
  342. SupportClass.StartBackgroundCalls(this.RegionPingThreaded, 0);
  343. #else
  344. SupportClass.StartBackgroundCalls(this.RegionPingThreaded, 0, "RegionPing_" + this.region.Code+"_"+this.region.Cluster);
  345. #endif
  346. return true;
  347. }
  348. protected internal bool RegionPingThreaded()
  349. {
  350. this.region.Ping = PingWhenFailed;
  351. float rttSum = 0.0f;
  352. int replyCount = 0;
  353. Stopwatch sw = new Stopwatch();
  354. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  355. {
  356. bool overtime = false;
  357. sw.Reset();
  358. sw.Start();
  359. try
  360. {
  361. this.ping.StartPing(this.regionAddress);
  362. }
  363. catch (Exception e)
  364. {
  365. System.Diagnostics.Debug.WriteLine("RegionPinger.RegionPingThreaded() catched an exception for ping.StartPing(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  366. break;
  367. }
  368. while (!this.ping.Done())
  369. {
  370. if (sw.ElapsedMilliseconds >= MaxMilliseconsPerPing)
  371. {
  372. overtime = true;
  373. break;
  374. }
  375. #if !NETFX_CORE
  376. System.Threading.Thread.Sleep(0);
  377. #endif
  378. }
  379. sw.Stop();
  380. int rtt = (int)sw.ElapsedMilliseconds;
  381. this.rttResults.Add(rtt);
  382. if (IgnoreInitialAttempt && this.CurrentAttempt == 0)
  383. {
  384. // do nothing.
  385. }
  386. else if (this.ping.Successful && !overtime)
  387. {
  388. rttSum += rtt;
  389. replyCount++;
  390. this.region.Ping = (int)((rttSum) / replyCount);
  391. }
  392. #if !NETFX_CORE
  393. System.Threading.Thread.Sleep(10);
  394. #endif
  395. }
  396. //Debug.Log("Done: "+ this.region.Code);
  397. this.Done = true;
  398. this.ping.Dispose();
  399. this.onDoneCall(this.region);
  400. return false;
  401. }
  402. #if SUPPORTED_UNITY
  403. /// <remarks>
  404. /// Affected by frame-rate of app, as this Coroutine checks the socket for a result once per frame.
  405. /// </remarks>
  406. protected internal IEnumerator RegionPingCoroutine()
  407. {
  408. this.region.Ping = PingWhenFailed;
  409. float rttSum = 0.0f;
  410. int replyCount = 0;
  411. Stopwatch sw = new Stopwatch();
  412. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  413. {
  414. bool overtime = false;
  415. sw.Reset();
  416. sw.Start();
  417. try
  418. {
  419. this.ping.StartPing(this.regionAddress);
  420. }
  421. catch (Exception e)
  422. {
  423. Debug.Log("catched: " + e);
  424. break;
  425. }
  426. while (!this.ping.Done())
  427. {
  428. if (sw.ElapsedMilliseconds >= MaxMilliseconsPerPing)
  429. {
  430. overtime = true;
  431. break;
  432. }
  433. yield return 0; // keep this loop tight, to avoid adding local lag to rtt.
  434. }
  435. sw.Stop();
  436. int rtt = (int)sw.ElapsedMilliseconds;
  437. this.rttResults.Add(rtt);
  438. if (IgnoreInitialAttempt && this.CurrentAttempt == 0)
  439. {
  440. // do nothing.
  441. }
  442. else if (this.ping.Successful && !overtime)
  443. {
  444. rttSum += rtt;
  445. replyCount++;
  446. this.region.Ping = (int)((rttSum) / replyCount);
  447. }
  448. yield return new WaitForSeconds(0.1f);
  449. }
  450. //Debug.Log("Done: "+ this.region.Code);
  451. this.Done = true;
  452. this.ping.Dispose();
  453. this.onDoneCall(this.region);
  454. yield return null;
  455. }
  456. #endif
  457. public string GetResults()
  458. {
  459. return string.Format("{0}: {1} ({2})", this.region.Code, this.region.Ping, this.rttResults.ToStringFull());
  460. }
  461. /// <summary>
  462. /// Attempts to resolve a hostname into an IP string or returns empty string if that fails.
  463. /// </summary>
  464. /// <remarks>
  465. /// To be compatible with most platforms, the address family is checked like this:<br/>
  466. /// if (ipAddress.AddressFamily.ToString().Contains("6")) // ipv6...
  467. /// </remarks>
  468. /// <param name="hostName">Hostname to resolve.</param>
  469. /// <returns>IP string or empty string if resolution fails</returns>
  470. public static string ResolveHost(string hostName)
  471. {
  472. if (hostName.StartsWith("wss://"))
  473. {
  474. hostName = hostName.Substring(6);
  475. }
  476. if (hostName.StartsWith("ws://"))
  477. {
  478. hostName = hostName.Substring(5);
  479. }
  480. string ipv4Address = string.Empty;
  481. try
  482. {
  483. #if UNITY_WSA || NETFX_CORE || UNITY_WEBGL
  484. return hostName;
  485. #else
  486. IPAddress[] address = Dns.GetHostAddresses(hostName);
  487. if (address.Length == 1)
  488. {
  489. return address[0].ToString();
  490. }
  491. // if we got more addresses, try to pick a IPv6 one
  492. // checking ipAddress.ToString() means we don't have to import System.Net.Sockets, which is not available on some platforms (Metro)
  493. for (int index = 0; index < address.Length; index++)
  494. {
  495. IPAddress ipAddress = address[index];
  496. if (ipAddress != null)
  497. {
  498. if (ipAddress.ToString().Contains(":"))
  499. {
  500. return ipAddress.ToString();
  501. }
  502. if (string.IsNullOrEmpty(ipv4Address))
  503. {
  504. ipv4Address = address.ToString();
  505. }
  506. }
  507. }
  508. #endif
  509. }
  510. catch (System.Exception e)
  511. {
  512. System.Diagnostics.Debug.WriteLine("RegionPinger.ResolveHost() catched an exception for Dns.GetHostAddresses(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  513. }
  514. return ipv4Address;
  515. }
  516. }
  517. }