NetworkDiscoveryBase.cs 15 KB

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