123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- using System;
- using System.Net;
- using System.Net.Sockets;
- using System.Threading.Tasks;
- using UnityEngine;
- // Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
- // forked from https://github.com/in0finite/MirrorNetworkDiscovery
- // Both are MIT Licensed
- namespace Mirror.Discovery
- {
- /// <summary>
- /// Base implementation for Network Discovery. Extend this component
- /// to provide custom discovery with game specific data
- /// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
- /// </summary>
- [DisallowMultipleComponent]
- [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
- public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
- where Request : NetworkMessage
- where Response : NetworkMessage
- {
- public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
- // each game should have a random unique handshake, this way you can tell if this is the same game or not
- [HideInInspector]
- public long secretHandshake;
- [SerializeField]
- [Tooltip("The UDP port the server will listen for multi-cast messages")]
- protected int serverBroadcastListenPort = 47777;
- [SerializeField]
- [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
- public bool enableActiveDiscovery = true;
- [SerializeField]
- [Tooltip("Time in seconds between multi-cast messages")]
- [Range(1, 60)]
- float ActiveDiscoveryInterval = 3;
- protected UdpClient serverUdpClient;
- protected UdpClient clientUdpClient;
- #if UNITY_EDITOR
- void OnValidate()
- {
- if (secretHandshake == 0)
- {
- secretHandshake = RandomLong();
- UnityEditor.Undo.RecordObject(this, "Set secret handshake");
- }
- }
- #endif
- public static long RandomLong()
- {
- int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
- int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
- return value1 + ((long)value2 << 32);
- }
- /// <summary>
- /// virtual so that inheriting classes' Start() can call base.Start() too
- /// </summary>
- public virtual void Start()
- {
- // Server mode? then start advertising
- #if UNITY_SERVER
- AdvertiseServer();
- #endif
- }
- // Ensure the ports are cleared no matter when Game/Unity UI exits
- void OnApplicationQuit()
- {
- //Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
- Shutdown();
- }
- void OnDisable()
- {
- //Debug.Log("NetworkDiscoveryBase OnDisable");
- Shutdown();
- }
- void OnDestroy()
- {
- //Debug.Log("NetworkDiscoveryBase OnDestroy");
- Shutdown();
- }
- void Shutdown()
- {
- if (serverUdpClient != null)
- {
- try
- {
- serverUdpClient.Close();
- }
- catch (Exception)
- {
- // it is just close, swallow the error
- }
- serverUdpClient = null;
- }
- if (clientUdpClient != null)
- {
- try
- {
- clientUdpClient.Close();
- }
- catch (Exception)
- {
- // it is just close, swallow the error
- }
- clientUdpClient = null;
- }
- CancelInvoke();
- }
- #region Server
- /// <summary>
- /// Advertise this server in the local network
- /// </summary>
- public void AdvertiseServer()
- {
- if (!SupportedOnThisPlatform)
- throw new PlatformNotSupportedException("Network discovery not supported in this platform");
- StopDiscovery();
- // Setup port -- may throw exception
- serverUdpClient = new UdpClient(serverBroadcastListenPort)
- {
- EnableBroadcast = true,
- MulticastLoopback = false
- };
- // listen for client pings
- _ = ServerListenAsync();
- }
- public async Task ServerListenAsync()
- {
- while (true)
- {
- try
- {
- await ReceiveRequestAsync(serverUdpClient);
- }
- catch (ObjectDisposedException)
- {
- // socket has been closed
- break;
- }
- catch (Exception)
- {
- }
- }
- }
- async Task ReceiveRequestAsync(UdpClient udpClient)
- {
- // only proceed if there is available data in network buffer, or otherwise Receive() will block
- // average time for UdpClient.Available : 10 us
- UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
- using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
- {
- long handshake = networkReader.ReadLong();
- if (handshake != secretHandshake)
- {
- // message is not for us
- throw new ProtocolViolationException("Invalid handshake");
- }
- Request request = networkReader.Read<Request>();
- ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
- }
- }
- /// <summary>
- /// Reply to the client to inform it of this server
- /// </summary>
- /// <remarks>
- /// Override if you wish to ignore server requests based on
- /// custom criteria such as language, full server game mode or difficulty
- /// </remarks>
- /// <param name="request">Request coming from client</param>
- /// <param name="endpoint">Address of the client that sent the request</param>
- protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
- {
- Response info = ProcessRequest(request, endpoint);
- if (info == null)
- return;
- using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
- {
- try
- {
- writer.WriteLong(secretHandshake);
- writer.Write(info);
- ArraySegment<byte> data = writer.ToArraySegment();
- // signature matches
- // send response
- serverUdpClient.Send(data.Array, data.Count, endpoint);
- }
- catch (Exception ex)
- {
- Debug.LogException(ex, this);
- }
- }
- }
- /// <summary>
- /// Process the request from a client
- /// </summary>
- /// <remarks>
- /// Override if you wish to provide more information to the clients
- /// such as the name of the host player
- /// </remarks>
- /// <param name="request">Request coming from client</param>
- /// <param name="endpoint">Address of the client that sent the request</param>
- /// <returns>The message to be sent back to the client or null</returns>
- protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
- #endregion
- #region Client
- /// <summary>
- /// Start Active Discovery
- /// </summary>
- public void StartDiscovery()
- {
- if (!SupportedOnThisPlatform)
- throw new PlatformNotSupportedException("Network discovery not supported in this platform");
- StopDiscovery();
- try
- {
- // Setup port
- clientUdpClient = new UdpClient(0)
- {
- EnableBroadcast = true,
- MulticastLoopback = false
- };
- }
- catch (Exception)
- {
- // Free the port if we took it
- //Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
- Shutdown();
- throw;
- }
- _ = ClientListenAsync();
- if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
- }
- /// <summary>
- /// Stop Active Discovery
- /// </summary>
- public void StopDiscovery()
- {
- //Debug.Log("NetworkDiscoveryBase StopDiscovery");
- Shutdown();
- }
- /// <summary>
- /// Awaits for server response
- /// </summary>
- /// <returns>ClientListenAsync Task</returns>
- public async Task ClientListenAsync()
- {
- while (true)
- {
- try
- {
- await ReceiveGameBroadcastAsync(clientUdpClient);
- }
- catch (ObjectDisposedException)
- {
- // socket was closed, no problem
- return;
- }
- catch (Exception ex)
- {
- Debug.LogException(ex);
- }
- }
- }
- /// <summary>
- /// Sends discovery request from client
- /// </summary>
- public void BroadcastDiscoveryRequest()
- {
- if (clientUdpClient == null)
- return;
- if (NetworkClient.isConnected)
- {
- StopDiscovery();
- return;
- }
- IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
- using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
- {
- writer.WriteLong(secretHandshake);
- try
- {
- Request request = GetRequest();
- writer.Write(request);
- ArraySegment<byte> data = writer.ToArraySegment();
- clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
- }
- catch (Exception)
- {
- // It is ok if we can't broadcast to one of the addresses
- }
- }
- }
- /// <summary>
- /// Create a message that will be broadcasted on the network to discover servers
- /// </summary>
- /// <remarks>
- /// Override if you wish to include additional data in the discovery message
- /// such as desired game mode, language, difficulty, etc... </remarks>
- /// <returns>An instance of ServerRequest with data to be broadcasted</returns>
- protected virtual Request GetRequest() => default;
- async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
- {
- // only proceed if there is available data in network buffer, or otherwise Receive() will block
- // average time for UdpClient.Available : 10 us
- UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
- using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
- {
- if (networkReader.ReadLong() != secretHandshake)
- return;
- Response response = networkReader.Read<Response>();
- ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
- }
- }
- /// <summary>
- /// Process the answer from a server
- /// </summary>
- /// <remarks>
- /// A client receives a reply from a server, this method processes the
- /// reply and raises an event
- /// </remarks>
- /// <param name="response">Response that came from the server</param>
- /// <param name="endpoint">Address of the server that replied</param>
- protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
- #endregion
- }
- }
|