KcpClient.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // kcp client logic abstracted into a class.
  2. // for use in Mirror, DOTSNET, testing, etc.
  3. using System;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. namespace kcp2k
  7. {
  8. public class KcpClient : KcpPeer
  9. {
  10. // IO
  11. protected Socket socket;
  12. public EndPoint remoteEndPoint;
  13. // expose local endpoint for users / relays / nat traversal etc.
  14. public EndPoint LocalEndPoint => socket?.LocalEndPoint;
  15. // config
  16. protected readonly KcpConfig config;
  17. // raw receive buffer always needs to be of 'MTU' size, even if
  18. // MaxMessageSize is larger. kcp always sends in MTU segments and having
  19. // a buffer smaller than MTU would silently drop excess data.
  20. // => we need the MTU to fit channel + message!
  21. // => protected because someone may overwrite RawReceive but still wants
  22. // to reuse the buffer.
  23. protected readonly byte[] rawReceiveBuffer;
  24. // callbacks
  25. // even for errors, to allow liraries to show popups etc.
  26. // instead of logging directly.
  27. // (string instead of Exception for ease of use and to avoid user panic)
  28. //
  29. // events are readonly, set in constructor.
  30. // this ensures they are always initialized when used.
  31. // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
  32. protected readonly Action OnConnectedCallback;
  33. protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
  34. protected readonly Action OnDisconnectedCallback;
  35. protected readonly Action<ErrorCode, string> OnErrorCallback;
  36. // state
  37. bool active = false; // active between when connect() and disconnect() are called
  38. public bool connected;
  39. public KcpClient(Action OnConnected,
  40. Action<ArraySegment<byte>, KcpChannel> OnData,
  41. Action OnDisconnected,
  42. Action<ErrorCode, string> OnError,
  43. KcpConfig config)
  44. : base(config, 0) // client has no cookie yet
  45. {
  46. // initialize callbacks first to ensure they can be used safely.
  47. OnConnectedCallback = OnConnected;
  48. OnDataCallback = OnData;
  49. OnDisconnectedCallback = OnDisconnected;
  50. OnErrorCallback = OnError;
  51. this.config = config;
  52. // create mtu sized receive buffer
  53. rawReceiveBuffer = new byte[config.Mtu];
  54. }
  55. // callbacks ///////////////////////////////////////////////////////////
  56. // some callbacks need to wrapped with some extra logic
  57. protected override void OnAuthenticated()
  58. {
  59. Log.Info($"KcpClient: OnConnected");
  60. connected = true;
  61. OnConnectedCallback();
  62. }
  63. protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
  64. OnDataCallback(message, channel);
  65. protected override void OnError(ErrorCode error, string message) =>
  66. OnErrorCallback(error, message);
  67. protected override void OnDisconnected()
  68. {
  69. Log.Info($"KcpClient: OnDisconnected");
  70. connected = false;
  71. socket?.Close();
  72. socket = null;
  73. remoteEndPoint = null;
  74. OnDisconnectedCallback();
  75. active = false;
  76. }
  77. ////////////////////////////////////////////////////////////////////////
  78. public void Connect(string address, ushort port)
  79. {
  80. if (connected)
  81. {
  82. Log.Warning("KcpClient: already connected!");
  83. return;
  84. }
  85. // resolve host name before creating peer.
  86. // fixes: https://github.com/MirrorNetworking/Mirror/issues/3361
  87. if (!Common.ResolveHostname(address, out IPAddress[] addresses))
  88. {
  89. // pass error to user callback. no need to log it manually.
  90. OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}");
  91. OnDisconnectedCallback();
  92. return;
  93. }
  94. // create fresh peer for each new session
  95. // client doesn't need secure cookie.
  96. Reset(config);
  97. Log.Info($"KcpClient: connect to {address}:{port}");
  98. // create socket
  99. remoteEndPoint = new IPEndPoint(addresses[0], port);
  100. socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
  101. active = true;
  102. // recv & send are called from main thread.
  103. // need to ensure this never blocks.
  104. // even a 1ms block per connection would stop us from scaling.
  105. socket.Blocking = false;
  106. // configure buffer sizes
  107. Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
  108. // bind to endpoint so we can use send/recv instead of sendto/recvfrom.
  109. socket.Connect(remoteEndPoint);
  110. // immediately send a hello message to the server.
  111. // server will call OnMessage and add the new connection.
  112. // note that this still has cookie=0 until we receive the server's hello.
  113. SendHello();
  114. }
  115. // io - input.
  116. // virtual so it may be modified for relays, etc.
  117. // call this while it returns true, to process all messages this tick.
  118. // returned ArraySegment is valid until next call to RawReceive.
  119. protected virtual bool RawReceive(out ArraySegment<byte> segment)
  120. {
  121. segment = default;
  122. if (socket == null) return false;
  123. try
  124. {
  125. return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
  126. }
  127. // for non-blocking sockets, Receive throws WouldBlock if there is
  128. // no message to read. that's okay. only log for other errors.
  129. catch (SocketException e)
  130. {
  131. // the other end closing the connection is not an 'error'.
  132. // but connections should never just end silently.
  133. // at least log a message for easier debugging.
  134. // for example, his can happen when connecting without a server.
  135. // see test: ConnectWithoutServer().
  136. Log.Info($"KcpClient: looks like the other end has closed the connection. This is fine: {e}");
  137. base.Disconnect();
  138. return false;
  139. }
  140. }
  141. // io - output.
  142. // virtual so it may be modified for relays, etc.
  143. protected override void RawSend(ArraySegment<byte> data)
  144. {
  145. try
  146. {
  147. socket.SendNonBlocking(data);
  148. }
  149. catch (SocketException e)
  150. {
  151. Log.Error($"KcpClient: Send failed: {e}");
  152. }
  153. }
  154. public void Send(ArraySegment<byte> segment, KcpChannel channel)
  155. {
  156. if (!connected)
  157. {
  158. Log.Warning("KcpClient: can't send because not connected!");
  159. return;
  160. }
  161. SendData(segment, channel);
  162. }
  163. // insert raw IO. usually from socket.Receive.
  164. // offset is useful for relays, where we may parse a header and then
  165. // feed the rest to kcp.
  166. public void RawInput(ArraySegment<byte> segment)
  167. {
  168. // ensure valid size: at least 1 byte for channel + 4 bytes for cookie
  169. if (segment.Count <= 5) return;
  170. // parse channel
  171. // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
  172. byte channel = segment.Array[segment.Offset + 0];
  173. // server messages always contain the security cookie.
  174. // parse it, assign if not assigned, warn if suddenly different.
  175. Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
  176. if (messageCookie == 0)
  177. {
  178. Log.Error($"KcpClient: received message with cookie=0, this should never happen. Server should always include the security cookie.");
  179. }
  180. if (cookie == 0)
  181. {
  182. cookie = messageCookie;
  183. Log.Info($"KcpClient: received initial cookie: {cookie}");
  184. }
  185. else if (cookie != messageCookie)
  186. {
  187. Log.Warning($"KcpClient: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
  188. return;
  189. }
  190. // parse message
  191. ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
  192. switch (channel)
  193. {
  194. case (byte)KcpChannel.Reliable:
  195. {
  196. OnRawInputReliable(message);
  197. break;
  198. }
  199. case (byte)KcpChannel.Unreliable:
  200. {
  201. OnRawInputUnreliable(message);
  202. break;
  203. }
  204. default:
  205. {
  206. // invalid channel indicates random internet noise.
  207. // servers may receive random UDP data.
  208. // just ignore it, but log for easier debugging.
  209. Log.Warning($"KcpClient: invalid channel header: {channel}, likely internet noise");
  210. break;
  211. }
  212. }
  213. }
  214. // process incoming messages. should be called before updating the world.
  215. // virtual because relay may need to inject their own ping or similar.
  216. public override void TickIncoming()
  217. {
  218. // recv on socket first, then process incoming
  219. // (even if we didn't receive anything. need to tick ping etc.)
  220. // (connection is null if not active)
  221. if (active)
  222. {
  223. while (RawReceive(out ArraySegment<byte> segment))
  224. RawInput(segment);
  225. }
  226. // RawReceive may have disconnected peer. active check again.
  227. if (active) base.TickIncoming();
  228. }
  229. // process outgoing messages. should be called after updating the world.
  230. // virtual because relay may need to inject their own ping or similar.
  231. public override void TickOutgoing()
  232. {
  233. // process outgoing while active
  234. if (active) base.TickOutgoing();
  235. }
  236. // process incoming and outgoing for convenience
  237. // => ideally call ProcessIncoming() before updating the world and
  238. // ProcessOutgoing() after updating the world for minimum latency
  239. public virtual void Tick()
  240. {
  241. TickIncoming();
  242. TickOutgoing();
  243. }
  244. }
  245. }