forked from EnlightenedOne/MirrorNetworkDiscovery
-
Notifications
You must be signed in to change notification settings - Fork 0
/
NetworkDiscovery.cs
283 lines (218 loc) · 9.93 KB
/
NetworkDiscovery.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using Guid = System.Guid;
using UnityEngine.Profiling;
using System.Collections;
using System;
using Assets.Scripts.Utility.Serialisation;
using Assets.Scripts.NetworkMessages;
namespace Mirror {
// Based on https://github.com/in0finite/MirrorNetworkDiscovery (license missing, contacted in0finite confirmed MIT license)
public class NetworkDiscovery : MonoBehaviour {
public static NetworkDiscovery instance { get; private set; }
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
public static event System.Action<DiscoveryInfo> onReceivedServerResponse = delegate { };
public string serverId { get; } = Guid.NewGuid().ToString();
// Change the text to make your game distinct from any other network traffic (this is the signature)
[HideInInspector]
public byte[] handshakeData { get; set; } = ByteStreamer.StreamToBytes("IAmAnExperimentalTeapotWith6FacetsOfTruth");
// This is the port the server will listen on and the port the client will multi-cast on
[SerializeField]
int m_serverBroadcastListenPort = 47777;
// This is the frequency in seconds at which the client will multi-cast out to all network interfaces to discover a server
[SerializeField]
int m_ActiveDiscoverySecondInterval = 3;
UdpClient serverUdpClient = null;
UdpClient clientUdpClient = null;
// This is a list of the NIC's found on start-up, I restart the client when the lobby screen loads and am not worried about refreshing this periodically
IPAddress[] cachedIPs = null;
// This is a cached copy of the current state of the servers game available for broadcast when a client calls the server
byte[] serverBroadcastPacket;
void Awake() {
if (instance != null) {
Destroy(gameObject);
}
instance = this;
DontDestroyOnLoad(gameObject);
// I normally only call this when the Lobby UI page is loaded but to make the sample cooler I made it start searching when you run the sample
ClientRunActiveDiscovery();
}
// I call this from my NetworkManage::OnStartServer
public bool ServerPassiveBroadcastGame(byte[] serverBroadcastPacket) {
if (!SupportedOnThisPlatform) {
return false;
}
StopDiscovery();
try {
// Setup port
serverUdpClient = new UdpClient(m_serverBroadcastListenPort);
serverUdpClient.EnableBroadcast = true;
serverUdpClient.MulticastLoopback = false;
} catch (Exception e) {
Debug.LogException(e);
// Free the port if we took it
if (serverUdpClient != null) {
NetworkDiscoveryUtility.RunSafe(() => {
ShutdownUdpClients();
});
}
return false;
}
this.serverBroadcastPacket = serverBroadcastPacket;
// TODO add coroutine to refresh server stats infrequently/make external update pathway for server to update broadcast packet
StartCoroutine(ServerListenCoroutine());
return true;
}
// I call this when my network manager acquires new players or other game state changes occur that I want to display in the lobby screen
public void UpdateServerBroadcastPacket(byte[] serverBroadcastPacket) {
this.serverBroadcastPacket = serverBroadcastPacket;
}
// I call this when the Lobby screen is loaded in my game
public bool ClientRunActiveDiscovery() {
if (!SupportedOnThisPlatform) {
return false;
}
StopDiscovery();
// Refresh the NIC list on entry to the lobby screen, could refresh on a timer if desired
cachedIPs = IPAddressUtility.GetBroadcastAdresses();
try {
// Setup port
clientUdpClient = new UdpClient(0);
clientUdpClient.EnableBroadcast = true;
clientUdpClient.MulticastLoopback = false;
} catch (Exception e) {
Debug.LogException(e);
// Free the port if we took it
if (clientUdpClient != null) {
NetworkDiscoveryUtility.RunSafe(() => {
ShutdownUdpClients();
});
}
return false;
}
StartCoroutine(ClientListenCoroutine());
StartCoroutine(ClientBroadcastCoroutine());
return true;
}
// I call this when I leave the lobby menu and in my override of NetworkManager::OnStopServer
// Note that if plugged into a Mirrror sample the game continues being broadcast in the background
public void StopDiscovery() {
StopAllCoroutines();
ShutdownUdpClients();
}
// Ensure the ports are cleared no matter when Game/Unity UI exits
void OnApplicationQuit() {
ShutdownUdpClients();
}
void ShutdownUdpClients() {
if (serverUdpClient != null) {
serverUdpClient.Close();
serverUdpClient = null;
}
if (clientUdpClient != null) {
clientUdpClient.Close();
clientUdpClient = null;
}
}
IEnumerator ServerListenCoroutine() {
while (true) {
yield return new WaitForSecondsRealtime(0.3f);
// average time for this (including data receiving and processing): less than 100 us
Profiler.BeginSample("Receive broadcast");
NetworkDiscoveryUtility.RunSafe(() => {
var info = ReadDataFromUdpClient(serverUdpClient);
if (info != null) {
ServerOnClientBroadcast(info);
}
});
Profiler.EndSample();
}
}
void ServerOnClientBroadcast(DiscoveryInfo info) {
// This is the handshake, objective is not security just sheer improbability
if (handshakeData.Length == info.packetData.Length &&
handshakeData.SequenceEqual(info.packetData)) {
// signature matches
// send response
Profiler.BeginSample("Send response");
serverUdpClient.Send(serverBroadcastPacket, serverBroadcastPacket.Length, info.EndPoint);
Profiler.EndSample();
}
}
IEnumerator ClientListenCoroutine() {
while (true) {
yield return new WaitForSecondsRealtime(0.3f);
if (clientUdpClient == null) {
continue;
}
NetworkDiscoveryUtility.RunSafe(() => {
var info = ReadDataFromUdpClient(clientUdpClient);
if (info != null) {
OnReceivedServerResponse(info);
}
});
}
}
IEnumerator ClientBroadcastCoroutine() {
while (true) {
if (clientUdpClient == null) {
continue;
}
NetworkDiscoveryUtility.RunSafe(() => {
SendClientBroadcast();
});
yield return new WaitForSecondsRealtime(m_ActiveDiscoverySecondInterval);
}
}
void SendClientBroadcast() {
// We can't just send packet to 255.255.255.255 - the OS will only broadcast it to the network interface which the socket is bound to.
// We need to broadcast packet on every network interface.
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, m_serverBroadcastListenPort);
foreach (var address in cachedIPs) {
endPoint.Address = address;
SendDiscoveryRequest(endPoint);
}
}
void SendDiscoveryRequest(IPEndPoint endPoint) {
Profiler.BeginSample("UdpClient.Send");
try {
clientUdpClient.SendAsync(handshakeData, handshakeData.Length, endPoint);
} catch (SocketException ex) {
if (ex.ErrorCode == 10051) {
// Network is unreachable
// ignore this error
} else {
throw;
}
}
Profiler.EndSample();
}
void OnReceivedServerResponse(DiscoveryInfo info) {
// Validation is our capacity to decode the message, if the payload is so different we cant parse it we silently dump it!
NetworkDiscoveryUtility.RunSafe(() => {
info.unpackedData = (GameBroadcastPacket)ByteStreamer.StreamFromBytes(info.packetData);
}, false);
if (info.unpackedData != null) {
onReceivedServerResponse(info);
}
}
static DiscoveryInfo ReadDataFromUdpClient(UdpClient udpClient) {
// only proceed if there is available data in network buffer, or otherwise Receive() will block
// average time for UdpClient.Available : 10 us
if (udpClient.Available <= 0) {
return null;
}
Profiler.BeginSample("UdpClient.Receive");
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
byte[] receivedBytes = udpClient.Receive(ref remoteEP);
Profiler.EndSample();
if (remoteEP != null && receivedBytes != null && receivedBytes.Length > 0) {
return new DiscoveryInfo(remoteEP, receivedBytes);
}
return null;
}
}
}