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.

699 lines
23 KiB

4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 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.Threading;
  22. using System.Net;
  23. using System.Collections;
  24. using System.Collections.Generic;
  25. using System.Diagnostics;
  26. using ExitGames.Client.Photon;
  27. #if SUPPORTED_UNITY
  28. using UnityEngine;
  29. using Debug = UnityEngine.Debug;
  30. #endif
  31. #if SUPPORTED_UNITY || NETFX_CORE
  32. using Hashtable = ExitGames.Client.Photon.Hashtable;
  33. using SupportClass = ExitGames.Client.Photon.SupportClass;
  34. #endif
  35. /// <summary>
  36. /// Provides methods to work with Photon's regions (Photon Cloud) and can be use to find the one with best ping.
  37. /// </summary>
  38. /// <remarks>
  39. /// When a client uses a Name Server to fetch the list of available regions, the LoadBalancingClient will create a RegionHandler
  40. /// and provide it via the OnRegionListReceived callback.
  41. ///
  42. /// Your logic can decide to either connect to one of those regional servers, or it may use PingMinimumOfRegions to test
  43. /// which region provides the best ping.
  44. ///
  45. /// It makes sense to make clients "sticky" to a region when one gets selected.
  46. /// This can be achieved by storing the SummaryToCache value, once pinging was done.
  47. /// When the client connects again, the previous SummaryToCache helps limiting the number of regions to ping.
  48. /// 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.
  49. /// </remarks>
  50. public class RegionHandler
  51. {
  52. /// <summary>The implementation of PhotonPing to use for region pinging (Best Region detection).</summary>
  53. /// <remarks>Defaults to null, which means the Type is set automatically.</remarks>
  54. public static Type PingImplementation;
  55. /// <summary>A list of region names for the Photon Cloud. Set by the result of OpGetRegions().</summary>
  56. /// <remarks>
  57. /// Implement ILoadBalancingCallbacks and register for the callbacks to get OnRegionListReceived(RegionHandler regionHandler).
  58. /// You can also put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.
  59. /// </remarks>
  60. public List<Region> EnabledRegions { get; protected internal set; }
  61. private string availableRegionCodes;
  62. private Region bestRegionCache;
  63. /// <summary>
  64. /// When PingMinimumOfRegions was called and completed, the BestRegion is identified by best ping.
  65. /// </summary>
  66. public Region BestRegion
  67. {
  68. get
  69. {
  70. if (this.EnabledRegions == null)
  71. {
  72. return null;
  73. }
  74. if (this.bestRegionCache != null)
  75. {
  76. return this.bestRegionCache;
  77. }
  78. this.EnabledRegions.Sort((a, b) => a.Ping.CompareTo(b.Ping) );
  79. this.bestRegionCache = this.EnabledRegions[0];
  80. return this.bestRegionCache;
  81. }
  82. }
  83. /// <summary>
  84. /// This value summarizes the results of pinging currently available regions (after PingMinimumOfRegions finished).
  85. /// </summary>
  86. /// <remarks>
  87. /// This value should be stored in the client by the game logic.
  88. /// When connecting again, use it as previous summary to speed up pinging regions and to make the best region sticky for the client.
  89. /// </remarks>
  90. public string SummaryToCache
  91. {
  92. get
  93. {
  94. if (this.BestRegion != null) {
  95. return this.BestRegion.Code + ";" + this.BestRegion.Ping + ";" + this.availableRegionCodes;
  96. }
  97. return this.availableRegionCodes;
  98. }
  99. }
  100. public string GetResults()
  101. {
  102. StringBuilder sb = new StringBuilder();
  103. sb.AppendFormat("Region Pinging Result: {0}\n", this.BestRegion.ToString());
  104. foreach (RegionPinger region in this.pingerList)
  105. {
  106. sb.AppendFormat(region.GetResults() + "\n");
  107. }
  108. sb.AppendFormat("Previous summary: {0}", this.previousSummaryProvided);
  109. return sb.ToString();
  110. }
  111. public void SetRegions(OperationResponse opGetRegions)
  112. {
  113. if (opGetRegions.OperationCode != OperationCode.GetRegions)
  114. {
  115. return;
  116. }
  117. if (opGetRegions.ReturnCode != ErrorCode.Ok)
  118. {
  119. return;
  120. }
  121. string[] regions = opGetRegions[ParameterCode.Region] as string[];
  122. string[] servers = opGetRegions[ParameterCode.Address] as string[];
  123. if (regions == null || servers == null || regions.Length != servers.Length)
  124. {
  125. //TODO: log error
  126. //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());
  127. return;
  128. }
  129. this.bestRegionCache = null;
  130. this.EnabledRegions = new List<Region>(regions.Length);
  131. for (int i = 0; i < regions.Length; i++)
  132. {
  133. string server = servers[i];
  134. if (PortToPingOverride != 0)
  135. {
  136. server = LoadBalancingClient.ReplacePortWithAlternative(servers[i], PortToPingOverride);
  137. }
  138. Region tmp = new Region(regions[i], server);
  139. if (string.IsNullOrEmpty(tmp.Code))
  140. {
  141. continue;
  142. }
  143. this.EnabledRegions.Add(tmp);
  144. }
  145. Array.Sort(regions);
  146. this.availableRegionCodes = string.Join(",", regions);
  147. }
  148. private List<RegionPinger> pingerList = new List<RegionPinger>();
  149. private Action<RegionHandler> onCompleteCall;
  150. private int previousPing;
  151. public bool IsPinging { get; private set; }
  152. private string previousSummaryProvided;
  153. protected internal static ushort PortToPingOverride;
  154. public RegionHandler(ushort masterServerPortOverride = 0)
  155. {
  156. PortToPingOverride = masterServerPortOverride;
  157. }
  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 RegionHandler 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. lock (this.pingerList)
  213. {
  214. this.pingerList.Add(singlePinger);
  215. }
  216. singlePinger.Start();
  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. #if PING_VIA_COROUTINE
  230. MonoBehaviourEmpty.SelfDestroy();
  231. #endif
  232. }
  233. }
  234. private bool PingEnabledRegions()
  235. {
  236. if (this.EnabledRegions == null || this.EnabledRegions.Count == 0)
  237. {
  238. //TODO: log
  239. //Debug.LogError("No regions available. Maybe all got filtered out or the AppId is not correctly configured.");
  240. return false;
  241. }
  242. lock (this.pingerList)
  243. {
  244. this.pingerList.Clear();
  245. foreach (Region region in this.EnabledRegions)
  246. {
  247. RegionPinger rp = new RegionPinger(region, this.OnRegionDone);
  248. this.pingerList.Add(rp);
  249. rp.Start(); // TODO: check return value
  250. }
  251. }
  252. return true;
  253. }
  254. private void OnRegionDone(Region region)
  255. {
  256. lock (this.pingerList)
  257. {
  258. if (this.IsPinging == false)
  259. {
  260. return;
  261. }
  262. this.bestRegionCache = null;
  263. foreach (RegionPinger pinger in this.pingerList)
  264. {
  265. if (!pinger.Done)
  266. {
  267. return;
  268. }
  269. }
  270. this.IsPinging = false;
  271. }
  272. this.onCompleteCall(this);
  273. #if PING_VIA_COROUTINE
  274. MonoBehaviourEmpty.SelfDestroy();
  275. #endif
  276. }
  277. }
  278. public class RegionPinger
  279. {
  280. public static int Attempts = 5;
  281. public static bool IgnoreInitialAttempt = true;
  282. public static int MaxMilliseconsPerPing = 800; // enter a value you're sure some server can beat (have a lower rtt)
  283. public static int PingWhenFailed = Attempts * MaxMilliseconsPerPing;
  284. private Region region;
  285. private string regionAddress;
  286. public int CurrentAttempt = 0;
  287. public bool Done { get; private set; }
  288. private Action<Region> onDoneCall;
  289. private PhotonPing ping;
  290. private List<int> rttResults;
  291. public RegionPinger(Region region, Action<Region> onDoneCallback)
  292. {
  293. this.region = region;
  294. this.region.Ping = PingWhenFailed;
  295. this.Done = false;
  296. this.onDoneCall = onDoneCallback;
  297. }
  298. /// <summary>Selects the best fitting ping implementation or uses the one set in RegionHandler.PingImplementation.</summary>
  299. /// <returns>PhotonPing instance to use.</returns>
  300. private PhotonPing GetPingImplementation()
  301. {
  302. PhotonPing ping = null;
  303. // using each type explicitly in the conditional code, makes sure Unity doesn't strip the class / constructor.
  304. #if !UNITY_EDITOR && NETFX_CORE
  305. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingWindowsStore))
  306. {
  307. ping = new PingWindowsStore();
  308. }
  309. #elif NATIVE_SOCKETS || NO_SOCKET
  310. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingNativeDynamic))
  311. {
  312. ping = new PingNativeDynamic();
  313. }
  314. #elif UNITY_WEBGL
  315. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingHttp))
  316. {
  317. ping = new PingHttp();
  318. }
  319. #else
  320. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingMono))
  321. {
  322. ping = new PingMono();
  323. }
  324. #endif
  325. if (ping == null)
  326. {
  327. if (RegionHandler.PingImplementation != null)
  328. {
  329. ping = (PhotonPing)Activator.CreateInstance(RegionHandler.PingImplementation);
  330. }
  331. }
  332. return ping;
  333. }
  334. /// <summary>
  335. /// Starts the ping routine for the assigned region.
  336. /// </summary>
  337. /// <remarks>
  338. /// Pinging runs in a ThreadPool worker item or (if needed) in a Thread.
  339. /// WebGL runs pinging on the Main Thread as coroutine.
  340. /// </remarks>
  341. /// <returns>Always true.</returns>
  342. public bool Start()
  343. {
  344. // all addresses for Photon region servers will contain a :port ending. this needs to be removed first.
  345. // PhotonPing.StartPing() requires a plain (IP) address without port or protocol-prefix (on all but Windows 8.1 and WebGL platforms).
  346. string address = this.region.HostAndPort;
  347. int indexOfColon = address.LastIndexOf(':');
  348. if (indexOfColon > 1)
  349. {
  350. address = address.Substring(0, indexOfColon);
  351. }
  352. this.regionAddress = ResolveHost(address);
  353. this.ping = this.GetPingImplementation();
  354. this.Done = false;
  355. this.CurrentAttempt = 0;
  356. this.rttResults = new List<int>(Attempts);
  357. #if PING_VIA_COROUTINE
  358. MonoBehaviourEmpty.Instance.StartCoroutine(this.RegionPingCoroutine());
  359. #else
  360. bool queued = false;
  361. #if !NETFX_CORE
  362. try
  363. {
  364. queued = ThreadPool.QueueUserWorkItem(this.RegionPingPooled);
  365. }
  366. catch
  367. {
  368. queued = false;
  369. }
  370. #endif
  371. if (!queued)
  372. {
  373. SupportClass.StartBackgroundCalls(this.RegionPingThreaded, 0, "RegionPing_" + this.region.Code + "_" + this.region.Cluster);
  374. }
  375. #endif
  376. return true;
  377. }
  378. // wraps RegionPingThreaded() to get the signature compatible with ThreadPool.QueueUserWorkItem
  379. protected internal void RegionPingPooled(object context)
  380. {
  381. this.RegionPingThreaded();
  382. }
  383. protected internal bool RegionPingThreaded()
  384. {
  385. this.region.Ping = PingWhenFailed;
  386. float rttSum = 0.0f;
  387. int replyCount = 0;
  388. Stopwatch sw = new Stopwatch();
  389. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  390. {
  391. bool overtime = false;
  392. sw.Reset();
  393. sw.Start();
  394. try
  395. {
  396. this.ping.StartPing(this.regionAddress);
  397. }
  398. catch (Exception e)
  399. {
  400. System.Diagnostics.Debug.WriteLine("RegionPinger.RegionPingThreaded() catched an exception for ping.StartPing(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  401. break;
  402. }
  403. while (!this.ping.Done())
  404. {
  405. if (sw.ElapsedMilliseconds >= MaxMilliseconsPerPing)
  406. {
  407. overtime = true;
  408. break;
  409. }
  410. #if !NETFX_CORE
  411. System.Threading.Thread.Sleep(0);
  412. #endif
  413. }
  414. sw.Stop();
  415. int rtt = (int)sw.ElapsedMilliseconds;
  416. this.rttResults.Add(rtt);
  417. if (IgnoreInitialAttempt && this.CurrentAttempt == 0)
  418. {
  419. // do nothing.
  420. }
  421. else if (this.ping.Successful && !overtime)
  422. {
  423. rttSum += rtt;
  424. replyCount++;
  425. this.region.Ping = (int)((rttSum) / replyCount);
  426. }
  427. #if !NETFX_CORE
  428. System.Threading.Thread.Sleep(10);
  429. #endif
  430. }
  431. //Debug.Log("Done: "+ this.region.Code);
  432. this.Done = true;
  433. this.ping.Dispose();
  434. this.onDoneCall(this.region);
  435. return false;
  436. }
  437. #if SUPPORTED_UNITY
  438. /// <remarks>
  439. /// Affected by frame-rate of app, as this Coroutine checks the socket for a result once per frame.
  440. /// </remarks>
  441. protected internal IEnumerator RegionPingCoroutine()
  442. {
  443. this.region.Ping = PingWhenFailed;
  444. float rttSum = 0.0f;
  445. int replyCount = 0;
  446. Stopwatch sw = new Stopwatch();
  447. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  448. {
  449. bool overtime = false;
  450. sw.Reset();
  451. sw.Start();
  452. try
  453. {
  454. this.ping.StartPing(this.regionAddress);
  455. }
  456. catch (Exception e)
  457. {
  458. Debug.Log("catched: " + e);
  459. break;
  460. }
  461. while (!this.ping.Done())
  462. {
  463. if (sw.ElapsedMilliseconds >= MaxMilliseconsPerPing)
  464. {
  465. overtime = true;
  466. break;
  467. }
  468. yield return 0; // keep this loop tight, to avoid adding local lag to rtt.
  469. }
  470. sw.Stop();
  471. int rtt = (int)sw.ElapsedMilliseconds;
  472. this.rttResults.Add(rtt);
  473. if (IgnoreInitialAttempt && this.CurrentAttempt == 0)
  474. {
  475. // do nothing.
  476. }
  477. else if (this.ping.Successful && !overtime)
  478. {
  479. rttSum += rtt;
  480. replyCount++;
  481. this.region.Ping = (int)((rttSum) / replyCount);
  482. }
  483. yield return new WaitForSeconds(0.1f);
  484. }
  485. //Debug.Log("Done: "+ this.region.Code);
  486. this.Done = true;
  487. this.ping.Dispose();
  488. this.onDoneCall(this.region);
  489. yield return null;
  490. }
  491. #endif
  492. public string GetResults()
  493. {
  494. return string.Format("{0}: {1} ({2})", this.region.Code, this.region.Ping, this.rttResults.ToStringFull());
  495. }
  496. /// <summary>
  497. /// Attempts to resolve a hostname into an IP string or returns empty string if that fails.
  498. /// </summary>
  499. /// <remarks>
  500. /// To be compatible with most platforms, the address family is checked like this:<br/>
  501. /// if (ipAddress.AddressFamily.ToString().Contains("6")) // ipv6...
  502. /// </remarks>
  503. /// <param name="hostName">Hostname to resolve.</param>
  504. /// <returns>IP string or empty string if resolution fails</returns>
  505. public static string ResolveHost(string hostName)
  506. {
  507. if (hostName.StartsWith("wss://"))
  508. {
  509. hostName = hostName.Substring(6);
  510. }
  511. if (hostName.StartsWith("ws://"))
  512. {
  513. hostName = hostName.Substring(5);
  514. }
  515. string ipv4Address = string.Empty;
  516. try
  517. {
  518. #if UNITY_WSA || NETFX_CORE || UNITY_WEBGL
  519. return hostName;
  520. #else
  521. IPAddress[] address = Dns.GetHostAddresses(hostName);
  522. if (address.Length == 1)
  523. {
  524. return address[0].ToString();
  525. }
  526. // if we got more addresses, try to pick a IPv6 one
  527. // checking ipAddress.ToString() means we don't have to import System.Net.Sockets, which is not available on some platforms (Metro)
  528. for (int index = 0; index < address.Length; index++)
  529. {
  530. IPAddress ipAddress = address[index];
  531. if (ipAddress != null)
  532. {
  533. if (ipAddress.ToString().Contains(":"))
  534. {
  535. return ipAddress.ToString();
  536. }
  537. if (string.IsNullOrEmpty(ipv4Address))
  538. {
  539. ipv4Address = address.ToString();
  540. }
  541. }
  542. }
  543. #endif
  544. }
  545. catch (System.Exception e)
  546. {
  547. System.Diagnostics.Debug.WriteLine("RegionPinger.ResolveHost() catched an exception for Dns.GetHostAddresses(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  548. }
  549. return ipv4Address;
  550. }
  551. }
  552. #if PING_VIA_COROUTINE
  553. internal class MonoBehaviourEmpty : MonoBehaviour
  554. {
  555. private static bool instanceSet; // to avoid instance null check which may be incorrect
  556. private static MonoBehaviourEmpty instance;
  557. public static MonoBehaviourEmpty Instance
  558. {
  559. get
  560. {
  561. if (instanceSet)
  562. {
  563. return instance;
  564. }
  565. GameObject go = new GameObject();
  566. DontDestroyOnLoad(go);
  567. go.name = "RegionPinger";
  568. instance = go.AddComponent<MonoBehaviourEmpty>();
  569. instanceSet = true;
  570. return instance;
  571. }
  572. }
  573. public static void SelfDestroy()
  574. {
  575. if (instanceSet)
  576. {
  577. instanceSet = false;
  578. Destroy(instance.gameObject);
  579. }
  580. }
  581. }
  582. #endif
  583. }