NetworkDiscoveryBase.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. if (serverUdpClient != null)
  83. {
  84. try
  85. {
  86. serverUdpClient.Close();
  87. }
  88. catch (Exception)
  89. {
  90. // it is just close, swallow the error
  91. }
  92. serverUdpClient = null;
  93. }
  94. if (clientUdpClient != null)
  95. {
  96. try
  97. {
  98. clientUdpClient.Close();
  99. }
  100. catch (Exception)
  101. {
  102. // it is just close, swallow the error
  103. }
  104. clientUdpClient = null;
  105. }
  106. CancelInvoke();
  107. }
  108. #region Server
  109. /// <summary>
  110. /// Advertise this server in the local network
  111. /// </summary>
  112. public void AdvertiseServer()
  113. {
  114. if (!SupportedOnThisPlatform)
  115. throw new PlatformNotSupportedException("Network discovery not supported in this platform");
  116. StopDiscovery();
  117. // Setup port -- may throw exception
  118. serverUdpClient = new UdpClient(serverBroadcastListenPort)
  119. {
  120. EnableBroadcast = true,
  121. MulticastLoopback = false
  122. };
  123. // listen for client pings
  124. _ = ServerListenAsync();
  125. }
  126. public async Task ServerListenAsync()
  127. {
  128. while (true)
  129. {
  130. try
  131. {
  132. await ReceiveRequestAsync(serverUdpClient);
  133. }
  134. catch (ObjectDisposedException)
  135. {
  136. // socket has been closed
  137. break;
  138. }
  139. catch (Exception)
  140. {
  141. }
  142. }
  143. }
  144. async Task ReceiveRequestAsync(UdpClient udpClient)
  145. {
  146. // only proceed if there is available data in network buffer, or otherwise Receive() will block
  147. // average time for UdpClient.Available : 10 us
  148. UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
  149. using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
  150. {
  151. long handshake = networkReader.ReadLong();
  152. if (handshake != secretHandshake)
  153. {
  154. // message is not for us
  155. throw new ProtocolViolationException("Invalid handshake");
  156. }
  157. Request request = networkReader.Read<Request>();
  158. ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
  159. }
  160. }
  161. /// <summary>
  162. /// Reply to the client to inform it of this server
  163. /// </summary>
  164. /// <remarks>
  165. /// Override if you wish to ignore server requests based on
  166. /// custom criteria such as language, full server game mode or difficulty
  167. /// </remarks>
  168. /// <param name="request">Request coming from client</param>
  169. /// <param name="endpoint">Address of the client that sent the request</param>
  170. protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
  171. {
  172. Response info = ProcessRequest(request, endpoint);
  173. if (info == null)
  174. return;
  175. using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
  176. {
  177. try
  178. {
  179. writer.WriteLong(secretHandshake);
  180. writer.Write(info);
  181. ArraySegment<byte> data = writer.ToArraySegment();
  182. // signature matches
  183. // send response
  184. serverUdpClient.Send(data.Array, data.Count, endpoint);
  185. }
  186. catch (Exception ex)
  187. {
  188. Debug.LogException(ex, this);
  189. }
  190. }
  191. }
  192. /// <summary>
  193. /// Process the request from a client
  194. /// </summary>
  195. /// <remarks>
  196. /// Override if you wish to provide more information to the clients
  197. /// such as the name of the host player
  198. /// </remarks>
  199. /// <param name="request">Request coming from client</param>
  200. /// <param name="endpoint">Address of the client that sent the request</param>
  201. /// <returns>The message to be sent back to the client or null</returns>
  202. protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
  203. #endregion
  204. #region Client
  205. /// <summary>
  206. /// Start Active Discovery
  207. /// </summary>
  208. public void StartDiscovery()
  209. {
  210. if (!SupportedOnThisPlatform)
  211. throw new PlatformNotSupportedException("Network discovery not supported in this platform");
  212. StopDiscovery();
  213. try
  214. {
  215. // Setup port
  216. clientUdpClient = new UdpClient(0)
  217. {
  218. EnableBroadcast = true,
  219. MulticastLoopback = false
  220. };
  221. }
  222. catch (Exception)
  223. {
  224. // Free the port if we took it
  225. //Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
  226. Shutdown();
  227. throw;
  228. }
  229. _ = ClientListenAsync();
  230. if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
  231. }
  232. /// <summary>
  233. /// Stop Active Discovery
  234. /// </summary>
  235. public void StopDiscovery()
  236. {
  237. //Debug.Log("NetworkDiscoveryBase StopDiscovery");
  238. Shutdown();
  239. }
  240. /// <summary>
  241. /// Awaits for server response
  242. /// </summary>
  243. /// <returns>ClientListenAsync Task</returns>
  244. public async Task ClientListenAsync()
  245. {
  246. // while clientUpdClient to fix:
  247. // https://github.com/vis2k/Mirror/pull/2908
  248. //
  249. // If, you cancel discovery the clientUdpClient is set to null.
  250. // However, nothing cancels ClientListenAsync. If we change the if(true)
  251. // to check if the client is null. You can properly cancel the discovery,
  252. // and kill the listen thread.
  253. //
  254. // Prior to this fix, if you cancel the discovery search. It crashes the
  255. // thread, and is super noisy in the output. As well as causes issues on
  256. // the quest.
  257. while (clientUdpClient != null)
  258. {
  259. try
  260. {
  261. await ReceiveGameBroadcastAsync(clientUdpClient);
  262. }
  263. catch (ObjectDisposedException)
  264. {
  265. // socket was closed, no problem
  266. return;
  267. }
  268. catch (Exception ex)
  269. {
  270. Debug.LogException(ex);
  271. }
  272. }
  273. }
  274. /// <summary>
  275. /// Sends discovery request from client
  276. /// </summary>
  277. public void BroadcastDiscoveryRequest()
  278. {
  279. if (clientUdpClient == null)
  280. return;
  281. if (NetworkClient.isConnected)
  282. {
  283. StopDiscovery();
  284. return;
  285. }
  286. IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
  287. using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
  288. {
  289. writer.WriteLong(secretHandshake);
  290. try
  291. {
  292. Request request = GetRequest();
  293. writer.Write(request);
  294. ArraySegment<byte> data = writer.ToArraySegment();
  295. clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
  296. }
  297. catch (Exception)
  298. {
  299. // It is ok if we can't broadcast to one of the addresses
  300. }
  301. }
  302. }
  303. /// <summary>
  304. /// Create a message that will be broadcasted on the network to discover servers
  305. /// </summary>
  306. /// <remarks>
  307. /// Override if you wish to include additional data in the discovery message
  308. /// such as desired game mode, language, difficulty, etc... </remarks>
  309. /// <returns>An instance of ServerRequest with data to be broadcasted</returns>
  310. protected virtual Request GetRequest() => default;
  311. async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
  312. {
  313. // only proceed if there is available data in network buffer, or otherwise Receive() will block
  314. // average time for UdpClient.Available : 10 us
  315. UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
  316. using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
  317. {
  318. if (networkReader.ReadLong() != secretHandshake)
  319. return;
  320. Response response = networkReader.Read<Response>();
  321. ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
  322. }
  323. }
  324. /// <summary>
  325. /// Process the answer from a server
  326. /// </summary>
  327. /// <remarks>
  328. /// A client receives a reply from a server, this method processes the
  329. /// reply and raises an event
  330. /// </remarks>
  331. /// <param name="response">Response that came from the server</param>
  332. /// <param name="endpoint">Address of the server that replied</param>
  333. protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
  334. #endregion
  335. }
  336. }