KcpClientConnection.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. using System.Net;
  2. using System.Net.Sockets;
  3. namespace kcp2k
  4. {
  5. public class KcpClientConnection : KcpConnection
  6. {
  7. // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
  8. // if MaxMessageSize is larger. kcp always sends in MTU
  9. // segments and having a buffer smaller than MTU would
  10. // silently drop excess data.
  11. // => we need the MTU to fit channel + message!
  12. readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
  13. // helper function to resolve host to IPAddress
  14. public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
  15. {
  16. try
  17. {
  18. // NOTE: dns lookup is blocking. this can take a second.
  19. addresses = Dns.GetHostAddresses(hostname);
  20. return addresses.Length >= 1;
  21. }
  22. catch (SocketException exception)
  23. {
  24. Log.Info($"Failed to resolve host: {hostname} reason: {exception}");
  25. addresses = null;
  26. return false;
  27. }
  28. }
  29. // EndPoint & Receive functions can be overwritten for where-allocation:
  30. // https://github.com/vis2k/where-allocation
  31. // NOTE: Client's SendTo doesn't allocate, don't need a virtual.
  32. protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) =>
  33. remoteEndPoint = new IPEndPoint(addresses[0], port);
  34. protected virtual int ReceiveFrom(byte[] buffer) =>
  35. socket.ReceiveFrom(buffer, ref remoteEndPoint);
  36. // if connections drop under heavy load, increase to OS limit.
  37. // if still not enough, increase the OS limit.
  38. void ConfigureSocketBufferSizes(bool maximizeSendReceiveBuffersToOSLimit)
  39. {
  40. if (maximizeSendReceiveBuffersToOSLimit)
  41. {
  42. // log initial size for comparison.
  43. // remember initial size for log comparison
  44. int initialReceive = socket.ReceiveBufferSize;
  45. int initialSend = socket.SendBufferSize;
  46. socket.SetReceiveBufferToOSLimit();
  47. socket.SetSendBufferToOSLimit();
  48. Log.Info($"KcpClient: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x) increased to OS limits!");
  49. }
  50. // otherwise still log the defaults for info.
  51. else Log.Info($"KcpClient: 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.");
  52. }
  53. public void Connect(string host,
  54. ushort port,
  55. bool noDelay,
  56. uint interval = Kcp.INTERVAL,
  57. int fastResend = 0,
  58. bool congestionWindow = true,
  59. uint sendWindowSize = Kcp.WND_SND,
  60. uint receiveWindowSize = Kcp.WND_RCV,
  61. int timeout = DEFAULT_TIMEOUT,
  62. uint maxRetransmits = Kcp.DEADLINK,
  63. bool maximizeSendReceiveBuffersToOSLimit = false)
  64. {
  65. Log.Info($"KcpClient: connect to {host}:{port}");
  66. // try resolve host name
  67. if (ResolveHostname(host, out IPAddress[] addresses))
  68. {
  69. // create remote endpoint
  70. CreateRemoteEndPoint(addresses, port);
  71. // create socket
  72. socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
  73. // configure buffer sizes
  74. ConfigureSocketBufferSizes(maximizeSendReceiveBuffersToOSLimit);
  75. // connect
  76. socket.Connect(remoteEndPoint);
  77. // set up kcp
  78. SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits);
  79. // client should send handshake to server as very first message
  80. SendHandshake();
  81. RawReceive();
  82. }
  83. // otherwise call OnDisconnected to let the user know.
  84. else
  85. {
  86. // pass error to user callback. no need to log it manually.
  87. OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {host}");
  88. OnDisconnected();
  89. }
  90. }
  91. // call from transport update
  92. public void RawReceive()
  93. {
  94. try
  95. {
  96. if (socket != null)
  97. {
  98. while (socket.Poll(0, SelectMode.SelectRead))
  99. {
  100. int msgLength = ReceiveFrom(rawReceiveBuffer);
  101. // IMPORTANT: detect if buffer was too small for the
  102. // received msgLength. otherwise the excess
  103. // data would be silently lost.
  104. // (see ReceiveFrom documentation)
  105. if (msgLength <= rawReceiveBuffer.Length)
  106. {
  107. //Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
  108. RawInput(rawReceiveBuffer, msgLength);
  109. }
  110. else
  111. {
  112. // pass error to user callback. no need to log it manually.
  113. OnError(ErrorCode.InvalidReceive, $"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
  114. Disconnect();
  115. }
  116. }
  117. }
  118. }
  119. // this is fine, the socket might have been closed in the other end
  120. catch (SocketException ex)
  121. {
  122. // the other end closing the connection is not an 'error'.
  123. // but connections should never just end silently.
  124. // at least log a message for easier debugging.
  125. Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}");
  126. Disconnect();
  127. }
  128. }
  129. protected override void Dispose()
  130. {
  131. socket.Close();
  132. socket = null;
  133. }
  134. protected override void RawSend(byte[] data, int length)
  135. {
  136. socket.Send(data, length, SocketFlags.None);
  137. }
  138. }
  139. }