NetworkDiscoveryBase.cs 14 KB

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