NetworkDiscoveryBase.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. using System;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. using System.Threading.Tasks;
  5. using UnityEngine;
  6. // Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
  7. // forked from https://github.com/in0finite/MirrorNetworkDiscovery
  8. // Both are MIT Licensed
  9. namespace Mirror.Discovery
  10. {
  11. /// <summary>
  12. /// Base implementation for Network Discovery. Extend this component
  13. /// to provide custom discovery with game specific data
  14. /// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
  15. /// </summary>
  16. [DisallowMultipleComponent]
  17. [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
  18. public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
  19. where Request : NetworkMessage
  20. where Response : NetworkMessage
  21. {
  22. public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
  23. // each game should have a random unique handshake, this way you can tell if this is the same game or not
  24. [HideInInspector]
  25. public long secretHandshake;
  26. [SerializeField]
  27. [Tooltip("The UDP port the server will listen for multi-cast messages")]
  28. protected int serverBroadcastListenPort = 47777;
  29. [SerializeField]
  30. [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
  31. public bool enableActiveDiscovery = true;
  32. [SerializeField]
  33. [Tooltip("Time in seconds between multi-cast messages")]
  34. [Range(1, 60)]
  35. float ActiveDiscoveryInterval = 3;
  36. // broadcast address needs to be configurable on iOS:
  37. // https://github.com/vis2k/Mirror/pull/3255
  38. public string BroadcastAddress = "";
  39. protected UdpClient serverUdpClient;
  40. protected UdpClient clientUdpClient;
  41. #if UNITY_EDITOR
  42. void OnValidate()
  43. {
  44. if (secretHandshake == 0)
  45. {
  46. secretHandshake = RandomLong();
  47. UnityEditor.Undo.RecordObject(this, "Set secret handshake");
  48. }
  49. }
  50. #endif
  51. public static long RandomLong()
  52. {
  53. int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
  54. int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
  55. return value1 + ((long)value2 << 32);
  56. }
  57. /// <summary>
  58. /// virtual so that inheriting classes' Start() can call base.Start() too
  59. /// </summary>
  60. public virtual void Start()
  61. {
  62. // Server mode? then start advertising
  63. #if UNITY_SERVER
  64. AdvertiseServer();
  65. #endif
  66. }
  67. // Ensure the ports are cleared no matter when Game/Unity UI exits
  68. void OnApplicationQuit()
  69. {
  70. //Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
  71. Shutdown();
  72. }
  73. void OnDisable()
  74. {
  75. //Debug.Log("NetworkDiscoveryBase OnDisable");
  76. Shutdown();
  77. }
  78. void OnDestroy()
  79. {
  80. //Debug.Log("NetworkDiscoveryBase OnDestroy");
  81. Shutdown();
  82. }
  83. void Shutdown()
  84. {
  85. EndpMulticastLock();
  86. if (serverUdpClient != null)
  87. {
  88. try
  89. {
  90. serverUdpClient.Close();
  91. }
  92. catch (Exception)
  93. {
  94. // it is just close, swallow the error
  95. }
  96. serverUdpClient = null;
  97. }
  98. if (clientUdpClient != null)
  99. {
  100. try
  101. {
  102. clientUdpClient.Close();
  103. }
  104. catch (Exception)
  105. {
  106. // it is just close, swallow the error
  107. }
  108. clientUdpClient = null;
  109. }
  110. CancelInvoke();
  111. }
  112. #region Server
  113. /// <summary>
  114. /// Advertise this server in the local network
  115. /// </summary>
  116. public void AdvertiseServer()
  117. {
  118. if (!SupportedOnThisPlatform)
  119. throw new PlatformNotSupportedException("Network discovery not supported in this platform");
  120. StopDiscovery();
  121. // Setup port -- may throw exception
  122. serverUdpClient = new UdpClient(serverBroadcastListenPort)
  123. {
  124. EnableBroadcast = true,
  125. MulticastLoopback = false
  126. };
  127. // listen for client pings
  128. _ = ServerListenAsync();
  129. }
  130. public async Task ServerListenAsync()
  131. {
  132. BeginMulticastLock();
  133. while (true)
  134. {
  135. try
  136. {
  137. await ReceiveRequestAsync(serverUdpClient);
  138. }
  139. catch (ObjectDisposedException)
  140. {
  141. // socket has been closed
  142. break;
  143. }
  144. catch (Exception)
  145. {
  146. }
  147. }
  148. }
  149. async Task ReceiveRequestAsync(UdpClient udpClient)
  150. {
  151. // only proceed if there is available data in network buffer, or otherwise Receive() will block
  152. // average time for UdpClient.Available : 10 us
  153. UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
  154. using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
  155. {
  156. long handshake = networkReader.ReadLong();
  157. if (handshake != secretHandshake)
  158. {
  159. // message is not for us
  160. throw new ProtocolViolationException("Invalid handshake");
  161. }
  162. Request request = networkReader.Read<Request>();
  163. ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
  164. }
  165. }
  166. /// <summary>
  167. /// Reply to the client to inform it of this server
  168. /// </summary>
  169. /// <remarks>
  170. /// Override if you wish to ignore server requests based on
  171. /// custom criteria such as language, full server game mode or difficulty
  172. /// </remarks>
  173. /// <param name="request">Request coming from client</param>
  174. /// <param name="endpoint">Address of the client that sent the request</param>
  175. protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
  176. {
  177. Response info = ProcessRequest(request, endpoint);
  178. if (info == null)
  179. return;
  180. using (NetworkWriterPooled writer = NetworkWriterPool.Get())
  181. {
  182. try
  183. {
  184. writer.WriteLong(secretHandshake);
  185. writer.Write(info);
  186. ArraySegment<byte> data = writer.ToArraySegment();
  187. // signature matches
  188. // send response
  189. serverUdpClient.Send(data.Array, data.Count, endpoint);
  190. }
  191. catch (Exception ex)
  192. {
  193. Debug.LogException(ex, this);
  194. }
  195. }
  196. }
  197. /// <summary>
  198. /// Process the request from a client
  199. /// </summary>
  200. /// <remarks>
  201. /// Override if you wish to provide more information to the clients
  202. /// such as the name of the host player
  203. /// </remarks>
  204. /// <param name="request">Request coming from client</param>
  205. /// <param name="endpoint">Address of the client that sent the request</param>
  206. /// <returns>The message to be sent back to the client or null</returns>
  207. protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
  208. // Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887
  209. #if UNITY_ANDROID
  210. AndroidJavaObject multicastLock;
  211. bool hasMulticastLock;
  212. #endif
  213. void BeginMulticastLock()
  214. {
  215. #if UNITY_ANDROID
  216. if (hasMulticastLock) return;
  217. if (Application.platform == RuntimePlatform.Android)
  218. {
  219. using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
  220. {
  221. using (var wifiManager = activity.Call<AndroidJavaObject>("getSystemService", "wifi"))
  222. {
  223. multicastLock = wifiManager.Call<AndroidJavaObject>("createMulticastLock", "lock");
  224. multicastLock.Call("acquire");
  225. hasMulticastLock = true;
  226. }
  227. }
  228. }
  229. #endif
  230. }
  231. void EndpMulticastLock()
  232. {
  233. #if UNITY_ANDROID
  234. if (!hasMulticastLock) return;
  235. multicastLock?.Call("release");
  236. hasMulticastLock = false;
  237. #endif
  238. }
  239. #endregion
  240. #region Client
  241. /// <summary>
  242. /// Start Active Discovery
  243. /// </summary>
  244. public void StartDiscovery()
  245. {
  246. if (!SupportedOnThisPlatform)
  247. throw new PlatformNotSupportedException("Network discovery not supported in this platform");
  248. StopDiscovery();
  249. try
  250. {
  251. // Setup port
  252. clientUdpClient = new UdpClient(0)
  253. {
  254. EnableBroadcast = true,
  255. MulticastLoopback = false
  256. };
  257. }
  258. catch (Exception)
  259. {
  260. // Free the port if we took it
  261. //Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
  262. Shutdown();
  263. throw;
  264. }
  265. _ = ClientListenAsync();
  266. if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
  267. }
  268. /// <summary>
  269. /// Stop Active Discovery
  270. /// </summary>
  271. public void StopDiscovery()
  272. {
  273. //Debug.Log("NetworkDiscoveryBase StopDiscovery");
  274. Shutdown();
  275. }
  276. /// <summary>
  277. /// Awaits for server response
  278. /// </summary>
  279. /// <returns>ClientListenAsync Task</returns>
  280. public async Task ClientListenAsync()
  281. {
  282. // while clientUpdClient to fix:
  283. // https://github.com/vis2k/Mirror/pull/2908
  284. //
  285. // If, you cancel discovery the clientUdpClient is set to null.
  286. // However, nothing cancels ClientListenAsync. If we change the if(true)
  287. // to check if the client is null. You can properly cancel the discovery,
  288. // and kill the listen thread.
  289. //
  290. // Prior to this fix, if you cancel the discovery search. It crashes the
  291. // thread, and is super noisy in the output. As well as causes issues on
  292. // the quest.
  293. while (clientUdpClient != null)
  294. {
  295. try
  296. {
  297. await ReceiveGameBroadcastAsync(clientUdpClient);
  298. }
  299. catch (ObjectDisposedException)
  300. {
  301. // socket was closed, no problem
  302. return;
  303. }
  304. catch (Exception ex)
  305. {
  306. Debug.LogException(ex);
  307. }
  308. }
  309. }
  310. /// <summary>
  311. /// Sends discovery request from client
  312. /// </summary>
  313. public void BroadcastDiscoveryRequest()
  314. {
  315. if (clientUdpClient == null)
  316. return;
  317. if (NetworkClient.isConnected)
  318. {
  319. StopDiscovery();
  320. return;
  321. }
  322. IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
  323. if (!string.IsNullOrWhiteSpace(BroadcastAddress))
  324. {
  325. try
  326. {
  327. endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort);
  328. }
  329. catch (Exception ex)
  330. {
  331. Debug.LogException(ex);
  332. }
  333. }
  334. using (NetworkWriterPooled writer = NetworkWriterPool.Get())
  335. {
  336. writer.WriteLong(secretHandshake);
  337. try
  338. {
  339. Request request = GetRequest();
  340. writer.Write(request);
  341. ArraySegment<byte> data = writer.ToArraySegment();
  342. clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
  343. }
  344. catch (Exception)
  345. {
  346. // It is ok if we can't broadcast to one of the addresses
  347. }
  348. }
  349. }
  350. /// <summary>
  351. /// Create a message that will be broadcasted on the network to discover servers
  352. /// </summary>
  353. /// <remarks>
  354. /// Override if you wish to include additional data in the discovery message
  355. /// such as desired game mode, language, difficulty, etc... </remarks>
  356. /// <returns>An instance of ServerRequest with data to be broadcasted</returns>
  357. protected virtual Request GetRequest() => default;
  358. async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
  359. {
  360. // only proceed if there is available data in network buffer, or otherwise Receive() will block
  361. // average time for UdpClient.Available : 10 us
  362. UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
  363. using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
  364. {
  365. if (networkReader.ReadLong() != secretHandshake)
  366. return;
  367. Response response = networkReader.Read<Response>();
  368. ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
  369. }
  370. }
  371. /// <summary>
  372. /// Process the answer from a server
  373. /// </summary>
  374. /// <remarks>
  375. /// A client receives a reply from a server, this method processes the
  376. /// reply and raises an event
  377. /// </remarks>
  378. /// <param name="response">Response that came from the server</param>
  379. /// <param name="endpoint">Address of the server that replied</param>
  380. protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
  381. #endregion
  382. }
  383. }