RegionHandler.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  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
  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. using System.Linq;
  28. #if SUPPORTED_UNITY
  29. using UnityEngine;
  30. using Debug = UnityEngine.Debug;
  31. #endif
  32. #if SUPPORTED_UNITY || NETFX_CORE
  33. using Hashtable = ExitGames.Client.Photon.Hashtable;
  34. using SupportClass = ExitGames.Client.Photon.SupportClass;
  35. #endif
  36. /// <summary>
  37. /// Provides methods to work with Photon's regions (Photon Cloud) and can be use to find the one with best ping.
  38. /// </summary>
  39. /// <remarks>
  40. /// When a client uses a Name Server to fetch the list of available regions, the LoadBalancingClient will create a RegionHandler
  41. /// and provide it via the OnRegionListReceived callback, as soon as the list is available. No pings were sent for Best Region selection yet.
  42. ///
  43. /// Your logic can decide to either connect to one of those regional servers, or it may use PingMinimumOfRegions to test
  44. /// which region provides the best ping. Alternatively the client may be set to connect to the Best Region (lowest ping), one or
  45. /// more regions get pinged.
  46. /// Not all regions will be pinged. As soon as the results are final, the client will connect to the best region,
  47. /// so you can check the ping results when connected to the Master Server.
  48. ///
  49. /// Regions gets pinged 5 times (RegionPinger.Attempts).
  50. /// Out of those, the worst rtt is discarded and the best will be counted two times for a weighted average.
  51. ///
  52. /// Usually UDP will be used to ping the Master Servers. In WebGL, WSS is used instead.
  53. ///
  54. /// It makes sense to make clients "sticky" to a region when one gets selected.
  55. /// This can be achieved by storing the SummaryToCache value, once pinging was done.
  56. /// When the client connects again, the previous SummaryToCache helps limiting the number of regions to ping.
  57. /// 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.
  58. /// </remarks>
  59. public class RegionHandler
  60. {
  61. /// <summary>The implementation of PhotonPing to use for region pinging (Best Region detection).</summary>
  62. /// <remarks>Defaults to null, which means the Type is set automatically.</remarks>
  63. public static Type PingImplementation;
  64. /// <summary>A list of region names for the Photon Cloud. Set by the result of OpGetRegions().</summary>
  65. /// <remarks>
  66. /// Implement ILoadBalancingCallbacks and register for the callbacks to get OnRegionListReceived(RegionHandler regionHandler).
  67. /// You can also put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.
  68. /// </remarks>
  69. public List<Region> EnabledRegions { get; protected internal set; }
  70. private string availableRegionCodes;
  71. private Region bestRegionCache;
  72. /// <summary>
  73. /// When PingMinimumOfRegions was called and completed, the BestRegion is identified by best ping.
  74. /// </summary>
  75. public Region BestRegion
  76. {
  77. get
  78. {
  79. if (this.EnabledRegions == null)
  80. {
  81. return null;
  82. }
  83. if (this.bestRegionCache != null)
  84. {
  85. return this.bestRegionCache;
  86. }
  87. this.EnabledRegions.Sort((a, b) => a.Ping.CompareTo(b.Ping));
  88. this.bestRegionCache = this.EnabledRegions[0];
  89. return this.bestRegionCache;
  90. }
  91. }
  92. /// <summary>
  93. /// This value summarizes the results of pinging currently available regions (after PingMinimumOfRegions finished).
  94. /// </summary>
  95. /// <remarks>
  96. /// This value should be stored in the client by the game logic.
  97. /// When connecting again, use it as previous summary to speed up pinging regions and to make the best region sticky for the client.
  98. /// </remarks>
  99. public string SummaryToCache
  100. {
  101. get
  102. {
  103. if (this.BestRegion != null && this.BestRegion.Ping < RegionPinger.MaxMillisecondsPerPing)
  104. {
  105. return this.BestRegion.Code + ";" + this.BestRegion.Ping + ";" + this.availableRegionCodes;
  106. }
  107. return this.availableRegionCodes;
  108. }
  109. }
  110. /// <summary>Provides a list of regions and their pings as string.</summary>
  111. public string GetResults()
  112. {
  113. StringBuilder sb = new StringBuilder();
  114. sb.AppendFormat("Region Pinging Result: {0}\n", this.BestRegion.ToString());
  115. foreach (RegionPinger region in this.pingerList)
  116. {
  117. sb.AppendLine(region.GetResults());
  118. }
  119. sb.AppendFormat("Previous summary: {0}", this.previousSummaryProvided);
  120. return sb.ToString();
  121. }
  122. /// <summary>Initializes the regions of this RegionHandler with values provided from the Name Server (as OperationResponse for OpGetRegions).</summary>
  123. public void SetRegions(OperationResponse opGetRegions)
  124. {
  125. if (opGetRegions.OperationCode != OperationCode.GetRegions)
  126. {
  127. return;
  128. }
  129. if (opGetRegions.ReturnCode != ErrorCode.Ok)
  130. {
  131. return;
  132. }
  133. string[] regions = opGetRegions[ParameterCode.Region] as string[];
  134. string[] servers = opGetRegions[ParameterCode.Address] as string[];
  135. if (regions == null || servers == null || regions.Length != servers.Length)
  136. {
  137. //TODO: log error
  138. //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());
  139. return;
  140. }
  141. this.bestRegionCache = null;
  142. this.EnabledRegions = new List<Region>(regions.Length);
  143. for (int i = 0; i < regions.Length; i++)
  144. {
  145. string server = servers[i];
  146. if (PortToPingOverride != 0)
  147. {
  148. server = LoadBalancingClient.ReplacePortWithAlternative(servers[i], PortToPingOverride);
  149. }
  150. Region tmp = new Region(regions[i], server);
  151. if (string.IsNullOrEmpty(tmp.Code))
  152. {
  153. continue;
  154. }
  155. this.EnabledRegions.Add(tmp);
  156. }
  157. Array.Sort(regions);
  158. this.availableRegionCodes = string.Join(",", regions);
  159. }
  160. private readonly List<RegionPinger> pingerList = new List<RegionPinger>();
  161. private Action<RegionHandler> onCompleteCall;
  162. private int previousPing;
  163. private string previousSummaryProvided;
  164. /// <summary>If non-zero, this port will be used to ping Master Servers on.</summary>
  165. protected internal static ushort PortToPingOverride;
  166. /// <summary>True if the available regions are being pinged currently.</summary>
  167. public bool IsPinging { get; private set; }
  168. /// <summary>True if the pinging of regions is being aborted.</summary>
  169. /// <see cref="Abort"/>
  170. public bool Aborted { get; private set; }
  171. /// <summary>If the region from a previous BestRegionSummary now has a ping higher than this limit, all regions get pinged again to find a better. Default: 90ms.</summary>
  172. /// <remarks>
  173. /// Pinging all regions takes time, which is why a BestRegionSummary gets stored.
  174. /// If that is available, the Best Region becomes sticky and is used again.
  175. /// This limit introduces an exception: Should the pre-defined best region have a ping worse than this, all regions are considered.
  176. /// </remarks>
  177. public int BestRegionSummaryPingLimit = 90;
  178. #if SUPPORTED_UNITY
  179. private MonoBehaviourEmpty emptyMonoBehavior;
  180. #endif
  181. /// <summary>Creates a new RegionHandler.</summary>
  182. /// <param name="masterServerPortOverride">If non-zero, this port will be used to ping Master Servers on.</param>
  183. public RegionHandler(ushort masterServerPortOverride = 0)
  184. {
  185. PortToPingOverride = masterServerPortOverride;
  186. }
  187. /// <summary>Starts the process of pinging of all available regions.</summary>
  188. /// <param name="onCompleteCallback">Provide a method to call when all ping results are available. Aborting the pings will also cancel the callback.</param>
  189. /// <param name="previousSummary">A BestRegionSummary from an earlier RegionHandler run. This makes a selected best region "sticky" and keeps ping times lower.</param>
  190. /// <returns>If pining the regions gets started now. False if the current state prevent this.</returns>
  191. public bool PingMinimumOfRegions(Action<RegionHandler> onCompleteCallback, string previousSummary)
  192. {
  193. if (this.EnabledRegions == null || this.EnabledRegions.Count == 0)
  194. {
  195. //TODO: log error
  196. //Debug.LogError("No regions available. Maybe all got filtered out or the AppId is not correctly configured.");
  197. return false;
  198. }
  199. if (this.IsPinging)
  200. {
  201. //TODO: log warning
  202. //Debug.LogWarning("PingMinimumOfRegions() skipped, because this RegionHandler is already pinging some regions.");
  203. return false;
  204. }
  205. this.Aborted = false;
  206. this.IsPinging = true;
  207. this.previousSummaryProvided = previousSummary;
  208. #if SUPPORTED_UNITY
  209. if (this.emptyMonoBehavior != null)
  210. {
  211. this.emptyMonoBehavior.SelfDestroy();
  212. }
  213. this.emptyMonoBehavior = MonoBehaviourEmpty.BuildInstance(nameof(RegionHandler));
  214. this.emptyMonoBehavior.onCompleteCall = onCompleteCallback;
  215. this.onCompleteCall = emptyMonoBehavior.CompleteOnMainThread;
  216. #else
  217. this.onCompleteCall = onCompleteCallback;
  218. #endif
  219. if (string.IsNullOrEmpty(previousSummary))
  220. {
  221. return this.PingEnabledRegions();
  222. }
  223. string[] values = previousSummary.Split(';');
  224. if (values.Length < 3)
  225. {
  226. return this.PingEnabledRegions();
  227. }
  228. int prevBestRegionPing;
  229. bool secondValueIsInt = Int32.TryParse(values[1], out prevBestRegionPing);
  230. if (!secondValueIsInt)
  231. {
  232. return this.PingEnabledRegions();
  233. }
  234. string prevBestRegionCode = values[0];
  235. string prevAvailableRegionCodes = values[2];
  236. if (string.IsNullOrEmpty(prevBestRegionCode))
  237. {
  238. return this.PingEnabledRegions();
  239. }
  240. if (string.IsNullOrEmpty(prevAvailableRegionCodes))
  241. {
  242. return this.PingEnabledRegions();
  243. }
  244. if (!this.availableRegionCodes.Equals(prevAvailableRegionCodes) || !this.availableRegionCodes.Contains(prevBestRegionCode))
  245. {
  246. return this.PingEnabledRegions();
  247. }
  248. if (prevBestRegionPing >= RegionPinger.PingWhenFailed)
  249. {
  250. return this.PingEnabledRegions();
  251. }
  252. // let's check only the preferred region to detect if it's still "good enough"
  253. this.previousPing = prevBestRegionPing;
  254. Region preferred = this.EnabledRegions.Find(r => r.Code.Equals(prevBestRegionCode));
  255. RegionPinger singlePinger = new RegionPinger(preferred, this.OnPreferredRegionPinged);
  256. lock (this.pingerList)
  257. {
  258. this.pingerList.Clear();
  259. this.pingerList.Add(singlePinger);
  260. }
  261. singlePinger.Start();
  262. return true;
  263. }
  264. /// <summary>Calling this will stop pinging the regions and suppress the onComplete callback.</summary>
  265. public void Abort()
  266. {
  267. if (this.Aborted)
  268. {
  269. return;
  270. }
  271. this.Aborted = true;
  272. lock (this.pingerList)
  273. {
  274. foreach (RegionPinger pinger in this.pingerList)
  275. {
  276. pinger.Abort();
  277. }
  278. }
  279. #if SUPPORTED_UNITY
  280. if (this.emptyMonoBehavior != null)
  281. {
  282. this.emptyMonoBehavior.SelfDestroy();
  283. }
  284. #endif
  285. }
  286. private void OnPreferredRegionPinged(Region preferredRegion)
  287. {
  288. if (preferredRegion.Ping > this.BestRegionSummaryPingLimit || preferredRegion.Ping > this.previousPing * 1.50f)
  289. {
  290. this.PingEnabledRegions();
  291. }
  292. else
  293. {
  294. this.IsPinging = false;
  295. this.onCompleteCall(this);
  296. }
  297. }
  298. private bool PingEnabledRegions()
  299. {
  300. if (this.EnabledRegions == null || this.EnabledRegions.Count == 0)
  301. {
  302. //TODO: log
  303. //Debug.LogError("No regions available. Maybe all got filtered out or the AppId is not correctly configured.");
  304. return false;
  305. }
  306. lock (this.pingerList)
  307. {
  308. this.pingerList.Clear();
  309. foreach (Region region in this.EnabledRegions)
  310. {
  311. RegionPinger rp = new RegionPinger(region, this.OnRegionDone);
  312. this.pingerList.Add(rp);
  313. rp.Start(); // TODO: check return value
  314. }
  315. }
  316. return true;
  317. }
  318. private void OnRegionDone(Region region)
  319. {
  320. lock (this.pingerList)
  321. {
  322. if (this.IsPinging == false)
  323. {
  324. return;
  325. }
  326. this.bestRegionCache = null;
  327. foreach (RegionPinger pinger in this.pingerList)
  328. {
  329. if (!pinger.Done)
  330. {
  331. return;
  332. }
  333. }
  334. this.IsPinging = false;
  335. }
  336. if (!this.Aborted)
  337. {
  338. this.onCompleteCall(this);
  339. }
  340. }
  341. }
  342. /// <summary>Wraps the ping attempts and workflow for a single region.</summary>
  343. public class RegionPinger
  344. {
  345. /// <summary>How often to ping a region.</summary>
  346. public static int Attempts = 5;
  347. /// <summary>How long to wait maximum for a response.</summary>
  348. public static int MaxMillisecondsPerPing = 800; // enter a value you're sure some server can beat (have a lower rtt)
  349. /// <summary>Ping result when pinging failed.</summary>
  350. public static int PingWhenFailed = Attempts * MaxMillisecondsPerPing;
  351. /// <summary>Current ping attempt count.</summary>
  352. public int CurrentAttempt = 0;
  353. /// <summary>True if all attempts are done or timed out.</summary>
  354. public bool Done { get; private set; }
  355. /// <summary>Set to true to abort pining this region.</summary>
  356. public bool Aborted { get; internal set; }
  357. private Action<Region> onDoneCall;
  358. private PhotonPing ping;
  359. private List<int> rttResults;
  360. private Region region;
  361. private string regionAddress;
  362. /// <summary>Initializes a RegionPinger for the given region.</summary>
  363. public RegionPinger(Region region, Action<Region> onDoneCallback)
  364. {
  365. this.region = region;
  366. this.region.Ping = PingWhenFailed;
  367. this.Done = false;
  368. this.onDoneCall = onDoneCallback;
  369. }
  370. /// <summary>Selects the best fitting ping implementation or uses the one set in RegionHandler.PingImplementation.</summary>
  371. /// <returns>PhotonPing instance to use.</returns>
  372. private PhotonPing GetPingImplementation()
  373. {
  374. PhotonPing ping = null;
  375. // using each type explicitly in the conditional code, makes sure Unity doesn't strip the class / constructor.
  376. #if !UNITY_EDITOR && NETFX_CORE
  377. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingWindowsStore))
  378. {
  379. ping = new PingWindowsStore();
  380. }
  381. #elif NATIVE_SOCKETS || NO_SOCKET
  382. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingNativeDynamic))
  383. {
  384. ping = new PingNativeDynamic();
  385. }
  386. #elif UNITY_WEBGL
  387. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingHttp))
  388. {
  389. ping = new PingHttp();
  390. }
  391. #else
  392. if (RegionHandler.PingImplementation == null || RegionHandler.PingImplementation == typeof(PingMono))
  393. {
  394. ping = new PingMono();
  395. }
  396. #endif
  397. if (ping == null)
  398. {
  399. if (RegionHandler.PingImplementation != null)
  400. {
  401. ping = (PhotonPing)Activator.CreateInstance(RegionHandler.PingImplementation);
  402. }
  403. }
  404. return ping;
  405. }
  406. /// <summary>
  407. /// Starts the ping routine for the assigned region.
  408. /// </summary>
  409. /// <remarks>
  410. /// Pinging runs in a ThreadPool worker item or (if needed) in a Thread.
  411. /// WebGL runs pinging on the Main Thread as coroutine.
  412. /// </remarks>
  413. /// <returns>True unless Aborted.</returns>
  414. public bool Start()
  415. {
  416. // all addresses for Photon region servers will contain a :port ending. this needs to be removed first.
  417. // PhotonPing.StartPing() requires a plain (IP) address without port or protocol-prefix (on all but Windows 8.1 and WebGL platforms).
  418. string address = this.region.HostAndPort;
  419. int indexOfColon = address.LastIndexOf(':');
  420. if (indexOfColon > 1)
  421. {
  422. address = address.Substring(0, indexOfColon);
  423. }
  424. this.regionAddress = ResolveHost(address);
  425. this.ping = this.GetPingImplementation();
  426. this.Done = false;
  427. this.CurrentAttempt = 0;
  428. this.rttResults = new List<int>(Attempts);
  429. if (this.Aborted)
  430. {
  431. return false;
  432. }
  433. #if PING_VIA_COROUTINE
  434. MonoBehaviourEmpty.BuildInstance("RegionPing_" + this.region.Code).StartCoroutineAndDestroy(this.RegionPingCoroutine());
  435. #else
  436. bool queued = false;
  437. #if !NETFX_CORE
  438. try
  439. {
  440. queued = ThreadPool.QueueUserWorkItem(o => this.RegionPingThreaded());
  441. }
  442. catch
  443. {
  444. queued = false;
  445. }
  446. #endif
  447. if (!queued)
  448. {
  449. SupportClass.StartBackgroundCalls(this.RegionPingThreaded, 0, "RegionPing_" + this.region.Code + "_" + this.region.Cluster);
  450. }
  451. #endif
  452. return true;
  453. }
  454. /// <summary>Calling this will stop pinging the regions and cancel the onComplete callback.</summary>
  455. protected internal void Abort()
  456. {
  457. this.Aborted = true;
  458. if (this.ping != null)
  459. {
  460. this.ping.Dispose();
  461. }
  462. }
  463. /// <summary>Pings the region. To be called by a thread.</summary>
  464. protected internal bool RegionPingThreaded()
  465. {
  466. this.region.Ping = PingWhenFailed;
  467. int rttSum = 0;
  468. int replyCount = 0;
  469. Stopwatch sw = new Stopwatch();
  470. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  471. {
  472. if (this.Aborted)
  473. {
  474. break;
  475. }
  476. sw.Reset();
  477. sw.Start();
  478. try
  479. {
  480. this.ping.StartPing(this.regionAddress);
  481. }
  482. catch (Exception e)
  483. {
  484. System.Diagnostics.Debug.WriteLine("RegionPinger.RegionPingThreaded() caught exception for ping.StartPing(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  485. break;
  486. }
  487. while (!this.ping.Done())
  488. {
  489. if (sw.ElapsedMilliseconds >= MaxMillisecondsPerPing)
  490. {
  491. // if ping.Done() did not become true in MaxMillisecondsPerPing, ping.Successful is false and we apply MaxMillisecondsPerPing as rtt below
  492. break;
  493. }
  494. #if !NETFX_CORE
  495. System.Threading.Thread.Sleep(1);
  496. #endif
  497. }
  498. sw.Stop();
  499. int rtt = this.ping.Successful ? (int)sw.ElapsedMilliseconds : MaxMillisecondsPerPing; // if the reply didn't match the sent ping
  500. this.rttResults.Add(rtt);
  501. rttSum += rtt;
  502. replyCount++;
  503. this.region.Ping = (int)((rttSum) / replyCount);
  504. #if !NETFX_CORE
  505. int i = 4;
  506. while (!this.ping.Done() && i > 0)
  507. {
  508. i--;
  509. System.Threading.Thread.Sleep(100);
  510. }
  511. System.Threading.Thread.Sleep(10);
  512. #endif
  513. }
  514. //Debug.Log("Done: "+ this.region.Code);
  515. this.Done = true;
  516. this.ping.Dispose();
  517. int bestRtt = this.rttResults.Min();
  518. int worstRtt = this.rttResults.Max();
  519. int weighedRttSum = rttSum - worstRtt + bestRtt;
  520. this.region.Ping = (int)(weighedRttSum / replyCount); // now, we can create a weighted ping value
  521. this.onDoneCall(this.region);
  522. return false;
  523. }
  524. #if SUPPORTED_UNITY
  525. /// <remarks>
  526. /// Affected by frame-rate of app, as this Coroutine checks the socket for a result once per frame.
  527. /// </remarks>
  528. protected internal IEnumerator RegionPingCoroutine()
  529. {
  530. this.region.Ping = PingWhenFailed;
  531. int rttSum = 0;
  532. int replyCount = 0;
  533. Stopwatch sw = new Stopwatch();
  534. for (this.CurrentAttempt = 0; this.CurrentAttempt < Attempts; this.CurrentAttempt++)
  535. {
  536. if (this.Aborted)
  537. {
  538. yield return null;
  539. }
  540. sw.Reset();
  541. sw.Start();
  542. try
  543. {
  544. this.ping.StartPing(this.regionAddress);
  545. }
  546. catch (Exception e)
  547. {
  548. Debug.Log("RegionPinger.RegionPingCoroutine() caught exception for ping.StartPing(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  549. break;
  550. }
  551. while (!this.ping.Done())
  552. {
  553. if (sw.ElapsedMilliseconds >= MaxMillisecondsPerPing)
  554. {
  555. // if ping.Done() did not become true in MaxMilliseconsPerPing, ping.Successful is false and we apply MaxMilliseconsPerPing as rtt below
  556. break;
  557. }
  558. yield return new WaitForSecondsRealtime(0.01f); // keep this loop tight, to avoid adding local lag to rtt.
  559. }
  560. sw.Stop();
  561. int rtt = this.ping.Successful ? (int)sw.ElapsedMilliseconds : MaxMillisecondsPerPing; // if the reply didn't match the sent ping
  562. this.rttResults.Add(rtt);
  563. rttSum += rtt;
  564. replyCount++;
  565. this.region.Ping = (int)((rttSum) / replyCount);
  566. int i = 4;
  567. while (!this.ping.Done() && i > 0)
  568. {
  569. i--;
  570. yield return new WaitForSeconds(0.1f);
  571. }
  572. yield return new WaitForSeconds(0.1f);
  573. }
  574. //Debug.Log("Done: "+ this.region.Code);
  575. this.Done = true;
  576. this.ping.Dispose();
  577. int bestRtt = this.rttResults.Min();
  578. int worstRtt = this.rttResults.Max();
  579. int weighedRttSum = rttSum - worstRtt + bestRtt;
  580. this.region.Ping = (int)(weighedRttSum / replyCount); // now, we can create a weighted ping value
  581. this.onDoneCall(this.region);
  582. yield return null;
  583. }
  584. #endif
  585. /// <summary>Gets this region's results as string summary.</summary>
  586. public string GetResults()
  587. {
  588. return string.Format("{0}: {1} ({2})", this.region.Code, this.region.Ping, this.rttResults.ToStringFull());
  589. }
  590. /// <summary>
  591. /// Attempts to resolve a hostname into an IP string or returns empty string if that fails.
  592. /// </summary>
  593. /// <remarks>
  594. /// To be compatible with most platforms, the address family is checked like this:<br/>
  595. /// if (ipAddress.AddressFamily.ToString().Contains("6")) // ipv6...
  596. /// </remarks>
  597. /// <param name="hostName">Hostname to resolve.</param>
  598. /// <returns>IP string or empty string if resolution fails</returns>
  599. public static string ResolveHost(string hostName)
  600. {
  601. if (hostName.StartsWith("wss://"))
  602. {
  603. hostName = hostName.Substring(6);
  604. }
  605. if (hostName.StartsWith("ws://"))
  606. {
  607. hostName = hostName.Substring(5);
  608. }
  609. string ipv4Address = string.Empty;
  610. try
  611. {
  612. #if UNITY_WSA || NETFX_CORE || UNITY_WEBGL
  613. return hostName;
  614. #else
  615. IPAddress[] address = Dns.GetHostAddresses(hostName);
  616. if (address.Length == 1)
  617. {
  618. return address[0].ToString();
  619. }
  620. // if we got more addresses, try to pick a IPv6 one
  621. // checking ipAddress.ToString() means we don't have to import System.Net.Sockets, which is not available on some platforms (Metro)
  622. for (int index = 0; index < address.Length; index++)
  623. {
  624. IPAddress ipAddress = address[index];
  625. if (ipAddress != null)
  626. {
  627. if (ipAddress.ToString().Contains(":"))
  628. {
  629. return ipAddress.ToString();
  630. }
  631. if (string.IsNullOrEmpty(ipv4Address))
  632. {
  633. ipv4Address = address.ToString();
  634. }
  635. }
  636. }
  637. #endif
  638. }
  639. catch (System.Exception e)
  640. {
  641. System.Diagnostics.Debug.WriteLine("RegionPinger.ResolveHost() caught an exception for Dns.GetHostAddresses(). Exception: " + e + " Source: " + e.Source + " Message: " + e.Message);
  642. }
  643. return ipv4Address;
  644. }
  645. }
  646. #if SUPPORTED_UNITY
  647. internal class MonoBehaviourEmpty : MonoBehaviour
  648. {
  649. internal Action<RegionHandler> onCompleteCall;
  650. private RegionHandler obj;
  651. public static MonoBehaviourEmpty BuildInstance(string id = null)
  652. {
  653. GameObject go = new GameObject(id ?? nameof(MonoBehaviourEmpty));
  654. DontDestroyOnLoad(go);
  655. return go.AddComponent<MonoBehaviourEmpty>();
  656. }
  657. public void SelfDestroy()
  658. {
  659. Destroy(this.gameObject);
  660. }
  661. void Update()
  662. {
  663. if (this.obj != null)
  664. {
  665. this.onCompleteCall(obj);
  666. this.obj = null;
  667. this.onCompleteCall = null;
  668. this.SelfDestroy();
  669. }
  670. }
  671. public void CompleteOnMainThread(RegionHandler obj)
  672. {
  673. this.obj = obj;
  674. }
  675. public void StartCoroutineAndDestroy(IEnumerator coroutine)
  676. {
  677. StartCoroutine(Routine());
  678. IEnumerator Routine()
  679. {
  680. yield return coroutine;
  681. this.SelfDestroy();
  682. }
  683. }
  684. }
  685. #endif
  686. }