// kcp client logic abstracted into a class. // for use in Mirror, DOTSNET, testing, etc. using System; using System.Net; using System.Net.Sockets; namespace kcp2k { public class KcpClient : KcpPeer { // IO protected Socket socket; public EndPoint remoteEndPoint; // expose local endpoint for users / relays / nat traversal etc. public EndPoint LocalEndPoint => socket?.LocalEndPoint; // config protected readonly KcpConfig config; // raw receive buffer always needs to be of 'MTU' size, even if // MaxMessageSize is larger. kcp always sends in MTU segments and having // a buffer smaller than MTU would silently drop excess data. // => we need the MTU to fit channel + message! // => protected because someone may overwrite RawReceive but still wants // to reuse the buffer. protected readonly byte[] rawReceiveBuffer; // callbacks // even for errors, to allow liraries to show popups etc. // instead of logging directly. // (string instead of Exception for ease of use and to avoid user panic) // // events are readonly, set in constructor. // this ensures they are always initialized when used. // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more protected readonly Action OnConnectedCallback; protected readonly Action, KcpChannel> OnDataCallback; protected readonly Action OnDisconnectedCallback; protected readonly Action OnErrorCallback; // state bool active = false; // active between when connect() and disconnect() are called public bool connected; public KcpClient(Action OnConnected, Action, KcpChannel> OnData, Action OnDisconnected, Action OnError, KcpConfig config) : base(config, 0) // client has no cookie yet { // initialize callbacks first to ensure they can be used safely. OnConnectedCallback = OnConnected; OnDataCallback = OnData; OnDisconnectedCallback = OnDisconnected; OnErrorCallback = OnError; this.config = config; // create mtu sized receive buffer rawReceiveBuffer = new byte[config.Mtu]; } // callbacks /////////////////////////////////////////////////////////// // some callbacks need to wrapped with some extra logic protected override void OnAuthenticated() { Log.Info($"[KCP] Client: OnConnected"); connected = true; OnConnectedCallback(); } protected override void OnData(ArraySegment message, KcpChannel channel) => OnDataCallback(message, channel); protected override void OnError(ErrorCode error, string message) => OnErrorCallback(error, message); protected override void OnDisconnected() { Log.Info($"[KCP] Client: OnDisconnected"); connected = false; socket?.Close(); socket = null; remoteEndPoint = null; OnDisconnectedCallback(); active = false; } //////////////////////////////////////////////////////////////////////// public void Connect(string address, ushort port) { if (connected) { Log.Warning("[KCP] Client: already connected!"); return; } // resolve host name before creating peer. // fixes: https://github.com/MirrorNetworking/Mirror/issues/3361 if (!Common.ResolveHostname(address, out IPAddress[] addresses)) { // pass error to user callback. no need to log it manually. OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}"); OnDisconnectedCallback(); return; } // create fresh peer for each new session // client doesn't need secure cookie. Reset(config); Log.Info($"[KCP] Client: connect to {address}:{port}"); // create socket remoteEndPoint = new IPEndPoint(addresses[0], port); socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); active = true; // recv & send are called from main thread. // need to ensure this never blocks. // even a 1ms block per connection would stop us from scaling. socket.Blocking = false; // configure buffer sizes Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize); // bind to endpoint so we can use send/recv instead of sendto/recvfrom. socket.Connect(remoteEndPoint); // immediately send a hello message to the server. // server will call OnMessage and add the new connection. // note that this still has cookie=0 until we receive the server's hello. SendHello(); } // io - input. // virtual so it may be modified for relays, etc. // call this while it returns true, to process all messages this tick. // returned ArraySegment is valid until next call to RawReceive. protected virtual bool RawReceive(out ArraySegment segment) { segment = default; if (socket == null) return false; try { return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment); } // for non-blocking sockets, Receive throws WouldBlock if there is // no message to read. that's okay. only log for other errors. catch (SocketException e) { // the other end closing the connection is not an 'error'. // but connections should never just end silently. // at least log a message for easier debugging. // for example, his can happen when connecting without a server. // see test: ConnectWithoutServer(). Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}"); base.Disconnect(); return false; } } // io - output. // virtual so it may be modified for relays, etc. protected override void RawSend(ArraySegment data) { // only if socket was connected / created yet. // users may call send functions without having connected, causing NRE. if (socket == null) return; try { socket.SendNonBlocking(data); } catch (SocketException e) { // SendDisconnect() sometimes gets a SocketException with // 'Connection Refused' if the other end already closed. // this is not an 'error', it's expected to happen. // but connections should never just end silently. // at least log a message for easier debugging. Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}"); // base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws } } public void Send(ArraySegment segment, KcpChannel channel) { if (!connected) { Log.Warning("[KCP] Client: can't send because not connected!"); return; } SendData(segment, channel); } // insert raw IO. usually from socket.Receive. // offset is useful for relays, where we may parse a header and then // feed the rest to kcp. public void RawInput(ArraySegment segment) { // ensure valid size: at least 1 byte for channel + 4 bytes for cookie if (segment.Count <= 5) return; // parse channel // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions byte channel = segment.Array[segment.Offset + 0]; // server messages always contain the security cookie. // parse it, assign if not assigned, warn if suddenly different. Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie); if (messageCookie == 0) { Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie."); } if (cookie == 0) { cookie = messageCookie; Log.Info($"[KCP] Client: received initial cookie: {cookie}"); } else if (cookie != messageCookie) { Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}."); return; } // parse message ArraySegment message = new ArraySegment(segment.Array, segment.Offset + 1+4, segment.Count - 1-4); switch (channel) { case (byte)KcpChannel.Reliable: { OnRawInputReliable(message); break; } case (byte)KcpChannel.Unreliable: { OnRawInputUnreliable(message); break; } default: { // invalid channel indicates random internet noise. // servers may receive random UDP data. // just ignore it, but log for easier debugging. Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise"); break; } } } // process incoming messages. should be called before updating the world. // virtual because relay may need to inject their own ping or similar. public override void TickIncoming() { // recv on socket first, then process incoming // (even if we didn't receive anything. need to tick ping etc.) // (connection is null if not active) if (active) { while (RawReceive(out ArraySegment segment)) RawInput(segment); } // RawReceive may have disconnected peer. active check again. if (active) base.TickIncoming(); } // process outgoing messages. should be called after updating the world. // virtual because relay may need to inject their own ping or similar. public override void TickOutgoing() { // process outgoing while active if (active) base.TickOutgoing(); } // process incoming and outgoing for convenience // => ideally call ProcessIncoming() before updating the world and // ProcessOutgoing() after updating the world for minimum latency public virtual void Tick() { TickIncoming(); TickOutgoing(); } } }