KcpServer.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. // kcp server logic abstracted into a class.
  2. // for use in Mirror, DOTSNET, testing, etc.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Net;
  6. using System.Net.Sockets;
  7. using System.Runtime.InteropServices;
  8. namespace kcp2k
  9. {
  10. public class KcpServer
  11. {
  12. // callbacks
  13. // even for errors, to allow liraries to show popups etc.
  14. // instead of logging directly.
  15. // (string instead of Exception for ease of use and to avoid user panic)
  16. //
  17. // events are readonly, set in constructor.
  18. // this ensures they are always initialized when used.
  19. // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
  20. protected readonly Action<int> OnConnected;
  21. protected readonly Action<int, ArraySegment<byte>, KcpChannel> OnData;
  22. protected readonly Action<int> OnDisconnected;
  23. protected readonly Action<int, ErrorCode, string> OnError;
  24. // configuration
  25. protected readonly KcpConfig config;
  26. // state
  27. protected Socket socket;
  28. EndPoint newClientEP;
  29. // expose local endpoint for users / relays / nat traversal etc.
  30. public EndPoint LocalEndPoint => socket?.LocalEndPoint;
  31. // raw receive buffer always needs to be of 'MTU' size, even if
  32. // MaxMessageSize is larger. kcp always sends in MTU segments and having
  33. // a buffer smaller than MTU would silently drop excess data.
  34. // => we need the mtu to fit channel + message!
  35. protected readonly byte[] rawReceiveBuffer;
  36. // connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
  37. public Dictionary<int, KcpServerConnection> connections =
  38. new Dictionary<int, KcpServerConnection>();
  39. public KcpServer(Action<int> OnConnected,
  40. Action<int, ArraySegment<byte>, KcpChannel> OnData,
  41. Action<int> OnDisconnected,
  42. Action<int, ErrorCode, string> OnError,
  43. KcpConfig config)
  44. {
  45. // initialize callbacks first to ensure they can be used safely.
  46. this.OnConnected = OnConnected;
  47. this.OnData = OnData;
  48. this.OnDisconnected = OnDisconnected;
  49. this.OnError = OnError;
  50. this.config = config;
  51. // create mtu sized receive buffer
  52. rawReceiveBuffer = new byte[config.Mtu];
  53. // create newClientEP either IPv4 or IPv6
  54. newClientEP = config.DualMode
  55. ? new IPEndPoint(IPAddress.IPv6Any, 0)
  56. : new IPEndPoint(IPAddress.Any, 0);
  57. }
  58. public virtual bool IsActive() => socket != null;
  59. static Socket CreateServerSocket(bool DualMode, ushort port)
  60. {
  61. if (DualMode)
  62. {
  63. // IPv6 socket with DualMode @ "::" : port
  64. Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
  65. // enabling DualMode may throw:
  66. // https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0
  67. // attempt it, otherwise log but continue
  68. // fixes: https://github.com/MirrorNetworking/Mirror/issues/3358
  69. try
  70. {
  71. socket.DualMode = true;
  72. }
  73. catch (NotSupportedException e)
  74. {
  75. Log.Warning($"Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}");
  76. }
  77. // for windows sockets, there's a rare issue where when using
  78. // a server socket with multiple clients, if one of the clients
  79. // is closed, the single server socket throws exceptions when
  80. // sending/receiving. even if the socket is made for N clients.
  81. //
  82. // this actually happened to one of our users:
  83. // https://github.com/MirrorNetworking/Mirror/issues/3611
  84. //
  85. // here's the in-depth explanation & solution:
  86. //
  87. // "As you may be aware, if a host receives a packet for a UDP
  88. // port that is not currently bound, it may send back an ICMP
  89. // "Port Unreachable" message. Whether or not it does this is
  90. // dependent on the firewall, private/public settings, etc.
  91. // On localhost, however, it will pretty much always send this
  92. // packet back.
  93. //
  94. // Now, on Windows (and only on Windows), by default, a received
  95. // ICMP Port Unreachable message will close the UDP socket that
  96. // sent it; hence, the next time you try to receive on the
  97. // socket, it will throw an exception because the socket has
  98. // been closed by the OS.
  99. //
  100. // Obviously, this causes a headache in the multi-client,
  101. // single-server socket set-up you have here, but luckily there
  102. // is a fix:
  103. //
  104. // You need to utilise the not-often-required SIO_UDP_CONNRESET
  105. // Winsock control code, which turns off this built-in behaviour
  106. // of automatically closing the socket.
  107. //
  108. // Note that this ioctl code is only supported on Windows
  109. // (XP and later), not on Linux, since it is provided by the
  110. // Winsock extensions. Of course, since the described behavior
  111. // is only the default behavior on Windows, this omission is not
  112. // a major loss. If you are attempting to create a
  113. // cross-platform library, you should cordon this off as
  114. // Windows-specific code."
  115. // https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
  116. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  117. {
  118. const uint IOC_IN = 0x80000000U;
  119. const uint IOC_VENDOR = 0x18000000U;
  120. const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
  121. socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
  122. }
  123. socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
  124. return socket;
  125. }
  126. else
  127. {
  128. // IPv4 socket @ "0.0.0.0" : port
  129. Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
  130. socket.Bind(new IPEndPoint(IPAddress.Any, port));
  131. return socket;
  132. }
  133. }
  134. public virtual void Start(ushort port)
  135. {
  136. // only start once
  137. if (socket != null)
  138. {
  139. Log.Warning("KcpServer: already started!");
  140. return;
  141. }
  142. // listen
  143. socket = CreateServerSocket(config.DualMode, port);
  144. // recv & send are called from main thread.
  145. // need to ensure this never blocks.
  146. // even a 1ms block per connection would stop us from scaling.
  147. socket.Blocking = false;
  148. // configure buffer sizes
  149. Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
  150. }
  151. public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
  152. {
  153. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  154. {
  155. connection.SendData(segment, channel);
  156. }
  157. }
  158. public void Disconnect(int connectionId)
  159. {
  160. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  161. {
  162. connection.Disconnect();
  163. }
  164. }
  165. // expose the whole IPEndPoint, not just the IP address. some need it.
  166. public IPEndPoint GetClientEndPoint(int connectionId)
  167. {
  168. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  169. {
  170. return connection.remoteEndPoint as IPEndPoint;
  171. }
  172. return null;
  173. }
  174. // io - input.
  175. // virtual so it may be modified for relays, nonalloc workaround, etc.
  176. // https://github.com/vis2k/where-allocation
  177. // bool return because not all receives may be valid.
  178. // for example, relay may expect a certain header.
  179. protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
  180. {
  181. segment = default;
  182. connectionId = 0;
  183. if (socket == null) return false;
  184. try
  185. {
  186. if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
  187. {
  188. // set connectionId to hash from endpoint
  189. connectionId = Common.ConnectionHash(newClientEP);
  190. return true;
  191. }
  192. }
  193. catch (SocketException e)
  194. {
  195. // NOTE: SocketException is not a subclass of IOException.
  196. // the other end closing the connection is not an 'error'.
  197. // but connections should never just end silently.
  198. // at least log a message for easier debugging.
  199. Log.Info($"KcpServer: ReceiveFrom failed: {e}");
  200. }
  201. return false;
  202. }
  203. // io - out.
  204. // virtual so it may be modified for relays, nonalloc workaround, etc.
  205. // relays may need to prefix connId (and remoteEndPoint would be same for all)
  206. protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
  207. {
  208. // get the connection's endpoint
  209. if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
  210. {
  211. Log.Warning($"KcpServer: RawSend invalid connectionId={connectionId}");
  212. return;
  213. }
  214. try
  215. {
  216. socket.SendToNonBlocking(data, connection.remoteEndPoint);
  217. }
  218. catch (SocketException e)
  219. {
  220. Log.Error($"KcpServer: SendTo failed: {e}");
  221. }
  222. }
  223. protected virtual KcpServerConnection CreateConnection(int connectionId)
  224. {
  225. // generate a random cookie for this connection to avoid UDP spoofing.
  226. // needs to be random, but without allocations to avoid GC.
  227. uint cookie = Common.GenerateCookie();
  228. // create empty connection without peer first.
  229. // we need it to set up peer callbacks.
  230. // afterwards we assign the peer.
  231. // events need to be wrapped with connectionIds
  232. KcpServerConnection connection = new KcpServerConnection(
  233. OnConnectedCallback,
  234. (message, channel) => OnData(connectionId, message, channel),
  235. OnDisconnectedCallback,
  236. (error, reason) => OnError(connectionId, error, reason),
  237. (data) => RawSend(connectionId, data),
  238. config,
  239. cookie,
  240. newClientEP);
  241. return connection;
  242. // setup authenticated event that also adds to connections
  243. void OnConnectedCallback(KcpServerConnection conn)
  244. {
  245. // add to connections dict after being authenticated.
  246. connections.Add(connectionId, conn);
  247. Log.Info($"KcpServer: added connection({connectionId})");
  248. // setup Data + Disconnected events only AFTER the
  249. // handshake. we don't want to fire OnServerDisconnected
  250. // every time we receive invalid random data from the
  251. // internet.
  252. // setup data event
  253. // finally, call mirror OnConnected event
  254. Log.Info($"KcpServer: OnConnected({connectionId})");
  255. OnConnected(connectionId);
  256. }
  257. void OnDisconnectedCallback()
  258. {
  259. // flag for removal
  260. // (can't remove directly because connection is updated
  261. // and event is called while iterating all connections)
  262. connectionsToRemove.Add(connectionId);
  263. // call mirror event
  264. Log.Info($"KcpServer: OnDisconnected({connectionId})");
  265. OnDisconnected(connectionId);
  266. }
  267. }
  268. // receive + add + process once.
  269. // best to call this as long as there is more data to receive.
  270. void ProcessMessage(ArraySegment<byte> segment, int connectionId)
  271. {
  272. //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
  273. // is this a new connection?
  274. if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
  275. {
  276. // create a new KcpConnection based on last received
  277. // EndPoint. can be overwritten for where-allocation.
  278. connection = CreateConnection(connectionId);
  279. // DO NOT add to connections yet. only if the first message
  280. // is actually the kcp handshake. otherwise it's either:
  281. // * random data from the internet
  282. // * or from a client connection that we just disconnected
  283. // but that hasn't realized it yet, still sending data
  284. // from last session that we should absolutely ignore.
  285. //
  286. //
  287. // TODO this allocates a new KcpConnection for each new
  288. // internet connection. not ideal, but C# UDP Receive
  289. // already allocated anyway.
  290. //
  291. // expecting a MAGIC byte[] would work, but sending the raw
  292. // UDP message without kcp's reliability will have low
  293. // probability of being received.
  294. //
  295. // for now, this is fine.
  296. // now input the message & process received ones
  297. // connected event was set up.
  298. // tick will process the first message and adds the
  299. // connection if it was the handshake.
  300. connection.RawInput(segment);
  301. connection.TickIncoming();
  302. // again, do not add to connections.
  303. // if the first message wasn't the kcp handshake then
  304. // connection will simply be garbage collected.
  305. }
  306. // existing connection: simply input the message into kcp
  307. else
  308. {
  309. connection.RawInput(segment);
  310. }
  311. }
  312. // process incoming messages. should be called before updating the world.
  313. // virtual because relay may need to inject their own ping or similar.
  314. readonly HashSet<int> connectionsToRemove = new HashSet<int>();
  315. public virtual void TickIncoming()
  316. {
  317. // input all received messages into kcp
  318. while (RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId))
  319. {
  320. ProcessMessage(segment, connectionId);
  321. }
  322. // process inputs for all server connections
  323. // (even if we didn't receive anything. need to tick ping etc.)
  324. foreach (KcpServerConnection connection in connections.Values)
  325. {
  326. connection.TickIncoming();
  327. }
  328. // remove disconnected connections
  329. // (can't do it in connection.OnDisconnected because Tick is called
  330. // while iterating connections)
  331. foreach (int connectionId in connectionsToRemove)
  332. {
  333. connections.Remove(connectionId);
  334. }
  335. connectionsToRemove.Clear();
  336. }
  337. // process outgoing messages. should be called after updating the world.
  338. // virtual because relay may need to inject their own ping or similar.
  339. public virtual void TickOutgoing()
  340. {
  341. // flush all server connections
  342. foreach (KcpServerConnection connection in connections.Values)
  343. {
  344. connection.TickOutgoing();
  345. }
  346. }
  347. // process incoming and outgoing for convenience.
  348. // => ideally call ProcessIncoming() before updating the world and
  349. // ProcessOutgoing() after updating the world for minimum latency
  350. public virtual void Tick()
  351. {
  352. TickIncoming();
  353. TickOutgoing();
  354. }
  355. public virtual void Stop()
  356. {
  357. // need to clear connections, otherwise they are in next session.
  358. // fixes https://github.com/vis2k/kcp2k/pull/47
  359. connections.Clear();
  360. socket?.Close();
  361. socket = null;
  362. }
  363. }
  364. }