KcpServer.cs 17 KB

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