NetworkDiscoveryBase.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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 (true)
  247. {
  248. try
  249. {
  250. await ReceiveGameBroadcastAsync(clientUdpClient);
  251. }
  252. catch (ObjectDisposedException)
  253. {
  254. // socket was closed, no problem
  255. return;
  256. }
  257. catch (Exception ex)
  258. {
  259. Debug.LogException(ex);
  260. }
  261. }
  262. }
  263. /// <summary>
  264. /// Sends discovery request from client
  265. /// </summary>
  266. public void BroadcastDiscoveryRequest()
  267. {
  268. if (clientUdpClient == null)
  269. return;
  270. if (NetworkClient.isConnected)
  271. {
  272. StopDiscovery();
  273. return;
  274. }
  275. IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
  276. using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
  277. {
  278. writer.WriteLong(secretHandshake);
  279. try
  280. {
  281. Request request = GetRequest();
  282. writer.Write(request);
  283. ArraySegment<byte> data = writer.ToArraySegment();
  284. clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
  285. }
  286. catch (Exception)
  287. {
  288. // It is ok if we can't broadcast to one of the addresses
  289. }
  290. }
  291. }
  292. /// <summary>
  293. /// Create a message that will be broadcasted on the network to discover servers
  294. /// </summary>
  295. /// <remarks>
  296. /// Override if you wish to include additional data in the discovery message
  297. /// such as desired game mode, language, difficulty, etc... </remarks>
  298. /// <returns>An instance of ServerRequest with data to be broadcasted</returns>
  299. protected virtual Request GetRequest() => default;
  300. async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
  301. {
  302. // only proceed if there is available data in network buffer, or otherwise Receive() will block
  303. // average time for UdpClient.Available : 10 us
  304. UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
  305. using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
  306. {
  307. if (networkReader.ReadLong() != secretHandshake)
  308. return;
  309. Response response = networkReader.Read<Response>();
  310. ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
  311. }
  312. }
  313. /// <summary>
  314. /// Process the answer from a server
  315. /// </summary>
  316. /// <remarks>
  317. /// A client receives a reply from a server, this method processes the
  318. /// reply and raises an event
  319. /// </remarks>
  320. /// <param name="response">Response that came from the server</param>
  321. /// <param name="endpoint">Address of the server that replied</param>
  322. protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
  323. #endregion
  324. }
  325. }