KcpServer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. namespace kcp2k
  8. {
  9. public class KcpServer
  10. {
  11. // events
  12. public Action<int> OnConnected;
  13. public Action<int, ArraySegment<byte>> OnData;
  14. public Action<int> OnDisconnected;
  15. // configuration
  16. // DualMode uses both IPv6 and IPv4. not all platforms support it.
  17. // (Nintendo Switch, etc.)
  18. public bool DualMode;
  19. // NoDelay is recommended to reduce latency. This also scales better
  20. // without buffers getting full.
  21. public bool NoDelay;
  22. // KCP internal update interval. 100ms is KCP default, but a lower
  23. // interval is recommended to minimize latency and to scale to more
  24. // networked entities.
  25. public uint Interval;
  26. // KCP fastresend parameter. Faster resend for the cost of higher
  27. // bandwidth.
  28. public int FastResend;
  29. // KCP 'NoCongestionWindow' is false by default. here we negate it for
  30. // ease of use. This can be disabled for high scale games if connections
  31. // choke regularly.
  32. public bool CongestionWindow;
  33. // KCP window size can be modified to support higher loads.
  34. // for example, Mirror Benchmark requires:
  35. // 128, 128 for 4k monsters
  36. // 512, 512 for 10k monsters
  37. // 8192, 8192 for 20k monsters
  38. public uint SendWindowSize;
  39. public uint ReceiveWindowSize;
  40. // timeout in milliseconds
  41. public int Timeout;
  42. // state
  43. protected Socket socket;
  44. EndPoint newClientEP;
  45. // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
  46. // if MaxMessageSize is larger. kcp always sends in MTU
  47. // segments and having a buffer smaller than MTU would
  48. // silently drop excess data.
  49. // => we need the mtu to fit channel + message!
  50. readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
  51. // connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
  52. public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
  53. public KcpServer(Action<int> OnConnected,
  54. Action<int, ArraySegment<byte>> OnData,
  55. Action<int> OnDisconnected,
  56. bool DualMode,
  57. bool NoDelay,
  58. uint Interval,
  59. int FastResend = 0,
  60. bool CongestionWindow = true,
  61. uint SendWindowSize = Kcp.WND_SND,
  62. uint ReceiveWindowSize = Kcp.WND_RCV,
  63. int Timeout = KcpConnection.DEFAULT_TIMEOUT)
  64. {
  65. this.OnConnected = OnConnected;
  66. this.OnData = OnData;
  67. this.OnDisconnected = OnDisconnected;
  68. this.DualMode = DualMode;
  69. this.NoDelay = NoDelay;
  70. this.Interval = Interval;
  71. this.FastResend = FastResend;
  72. this.CongestionWindow = CongestionWindow;
  73. this.SendWindowSize = SendWindowSize;
  74. this.ReceiveWindowSize = ReceiveWindowSize;
  75. this.Timeout = Timeout;
  76. // create newClientEP either IPv4 or IPv6
  77. newClientEP = DualMode
  78. ? new IPEndPoint(IPAddress.IPv6Any, 0)
  79. : new IPEndPoint(IPAddress.Any, 0);
  80. }
  81. public bool IsActive() => socket != null;
  82. public void Start(ushort port)
  83. {
  84. // only start once
  85. if (socket != null)
  86. {
  87. Log.Warning("KCP: server already started!");
  88. }
  89. // listen
  90. if (DualMode)
  91. {
  92. // IPv6 socket with DualMode
  93. socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
  94. socket.DualMode = true;
  95. socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
  96. }
  97. else
  98. {
  99. // IPv4 socket
  100. socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
  101. socket.Bind(new IPEndPoint(IPAddress.Any, port));
  102. }
  103. }
  104. public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
  105. {
  106. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  107. {
  108. connection.SendData(segment, channel);
  109. }
  110. }
  111. public void Disconnect(int connectionId)
  112. {
  113. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  114. {
  115. connection.Disconnect();
  116. }
  117. }
  118. public string GetClientAddress(int connectionId)
  119. {
  120. if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
  121. {
  122. return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
  123. }
  124. return "";
  125. }
  126. // EndPoint & Receive functions can be overwritten for where-allocation:
  127. // https://github.com/vis2k/where-allocation
  128. protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash)
  129. {
  130. // NOTE: ReceiveFrom allocates.
  131. // we pass our IPEndPoint to ReceiveFrom.
  132. // receive from calls newClientEP.Create(socketAddr).
  133. // IPEndPoint.Create always returns a new IPEndPoint.
  134. // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
  135. int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP);
  136. // calculate connectionHash from endpoint
  137. // NOTE: IPEndPoint.GetHashCode() allocates.
  138. // it calls m_Address.GetHashCode().
  139. // m_Address is an IPAddress.
  140. // GetHashCode() allocates for IPv6:
  141. // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
  142. //
  143. // => using only newClientEP.Port wouldn't work, because
  144. // different connections can have the same port.
  145. connectionHash = newClientEP.GetHashCode();
  146. return read;
  147. }
  148. protected virtual KcpServerConnection CreateConnection() =>
  149. new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
  150. // process incoming messages. should be called before updating the world.
  151. HashSet<int> connectionsToRemove = new HashSet<int>();
  152. public void TickIncoming()
  153. {
  154. while (socket != null && socket.Poll(0, SelectMode.SelectRead))
  155. {
  156. try
  157. {
  158. // receive
  159. int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId);
  160. //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
  161. // IMPORTANT: detect if buffer was too small for the received
  162. // msgLength. otherwise the excess data would be
  163. // silently lost.
  164. // (see ReceiveFrom documentation)
  165. if (msgLength <= rawReceiveBuffer.Length)
  166. {
  167. // is this a new connection?
  168. if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
  169. {
  170. // create a new KcpConnection based on last received
  171. // EndPoint. can be overwritten for where-allocation.
  172. connection = CreateConnection();
  173. // DO NOT add to connections yet. only if the first message
  174. // is actually the kcp handshake. otherwise it's either:
  175. // * random data from the internet
  176. // * or from a client connection that we just disconnected
  177. // but that hasn't realized it yet, still sending data
  178. // from last session that we should absolutely ignore.
  179. //
  180. //
  181. // TODO this allocates a new KcpConnection for each new
  182. // internet connection. not ideal, but C# UDP Receive
  183. // already allocated anyway.
  184. //
  185. // expecting a MAGIC byte[] would work, but sending the raw
  186. // UDP message without kcp's reliability will have low
  187. // probability of being received.
  188. //
  189. // for now, this is fine.
  190. // setup authenticated event that also adds to connections
  191. connection.OnAuthenticated = () =>
  192. {
  193. // only send handshake to client AFTER we received his
  194. // handshake in OnAuthenticated.
  195. // we don't want to reply to random internet messages
  196. // with handshakes each time.
  197. connection.SendHandshake();
  198. // add to connections dict after being authenticated.
  199. connections.Add(connectionId, connection);
  200. Log.Info($"KCP: server added connection({connectionId})");
  201. // setup Data + Disconnected events only AFTER the
  202. // handshake. we don't want to fire OnServerDisconnected
  203. // every time we receive invalid random data from the
  204. // internet.
  205. // setup data event
  206. connection.OnData = (message) =>
  207. {
  208. // call mirror event
  209. //Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
  210. OnData.Invoke(connectionId, message);
  211. };
  212. // setup disconnected event
  213. connection.OnDisconnected = () =>
  214. {
  215. // flag for removal
  216. // (can't remove directly because connection is updated
  217. // and event is called while iterating all connections)
  218. connectionsToRemove.Add(connectionId);
  219. // call mirror event
  220. Log.Info($"KCP: OnServerDisconnected({connectionId})");
  221. OnDisconnected.Invoke(connectionId);
  222. };
  223. // finally, call mirror OnConnected event
  224. Log.Info($"KCP: OnServerConnected({connectionId})");
  225. OnConnected.Invoke(connectionId);
  226. };
  227. // now input the message & process received ones
  228. // connected event was set up.
  229. // tick will process the first message and adds the
  230. // connection if it was the handshake.
  231. connection.RawInput(rawReceiveBuffer, msgLength);
  232. connection.TickIncoming();
  233. // again, do not add to connections.
  234. // if the first message wasn't the kcp handshake then
  235. // connection will simply be garbage collected.
  236. }
  237. // existing connection: simply input the message into kcp
  238. else
  239. {
  240. connection.RawInput(rawReceiveBuffer, msgLength);
  241. }
  242. }
  243. else
  244. {
  245. Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}.");
  246. Disconnect(connectionId);
  247. }
  248. }
  249. // this is fine, the socket might have been closed in the other end
  250. catch (SocketException) {}
  251. }
  252. // process inputs for all server connections
  253. // (even if we didn't receive anything. need to tick ping etc.)
  254. foreach (KcpServerConnection connection in connections.Values)
  255. {
  256. connection.TickIncoming();
  257. }
  258. // remove disconnected connections
  259. // (can't do it in connection.OnDisconnected because Tick is called
  260. // while iterating connections)
  261. foreach (int connectionId in connectionsToRemove)
  262. {
  263. connections.Remove(connectionId);
  264. }
  265. connectionsToRemove.Clear();
  266. }
  267. // process outgoing messages. should be called after updating the world.
  268. public void TickOutgoing()
  269. {
  270. // flush all server connections
  271. foreach (KcpServerConnection connection in connections.Values)
  272. {
  273. connection.TickOutgoing();
  274. }
  275. }
  276. // process incoming and outgoing for convenience.
  277. // => ideally call ProcessIncoming() before updating the world and
  278. // ProcessOutgoing() after updating the world for minimum latency
  279. public void Tick()
  280. {
  281. TickIncoming();
  282. TickOutgoing();
  283. }
  284. public void Stop()
  285. {
  286. socket?.Close();
  287. socket = null;
  288. }
  289. // pause/unpause to safely support mirror scene handling and to
  290. // immediately pause the receive while loop if needed.
  291. public void Pause()
  292. {
  293. foreach (KcpServerConnection connection in connections.Values)
  294. connection.Pause();
  295. }
  296. public void Unpause()
  297. {
  298. foreach (KcpServerConnection connection in connections.Values)
  299. connection.Unpause();
  300. }
  301. }
  302. }