123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- // 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<ArraySegment<byte>, KcpChannel> OnDataCallback;
- protected readonly Action OnDisconnectedCallback;
- protected readonly Action<ErrorCode, string> OnErrorCallback;
- // state
- bool active = false; // active between when connect() and disconnect() are called
- public bool connected;
- public KcpClient(Action OnConnected,
- Action<ArraySegment<byte>, KcpChannel> OnData,
- Action OnDisconnected,
- Action<ErrorCode, string> 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($"KcpClient: OnConnected");
- connected = true;
- OnConnectedCallback();
- }
- protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
- OnDataCallback(message, channel);
- protected override void OnError(ErrorCode error, string message) =>
- OnErrorCallback(error, message);
- protected override void OnDisconnected()
- {
- Log.Info($"KcpClient: OnDisconnected");
- connected = false;
- socket?.Close();
- socket = null;
- remoteEndPoint = null;
- OnDisconnectedCallback();
- active = false;
- }
- ////////////////////////////////////////////////////////////////////////
- public void Connect(string address, ushort port)
- {
- if (connected)
- {
- Log.Warning("KcpClient: 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($"KcpClient: 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<byte> 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($"KcpClient: 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<byte> data)
- {
- try
- {
- socket.SendNonBlocking(data);
- }
- catch (SocketException e)
- {
- Log.Error($"KcpClient: Send failed: {e}");
- }
- }
- public void Send(ArraySegment<byte> segment, KcpChannel channel)
- {
- if (!connected)
- {
- Log.Warning("KcpClient: 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<byte> 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($"KcpClient: received message with cookie=0, this should never happen. Server should always include the security cookie.");
- }
- if (cookie == 0)
- {
- cookie = messageCookie;
- Log.Info($"KcpClient: received initial cookie: {cookie}");
- }
- else if (cookie != messageCookie)
- {
- Log.Warning($"KcpClient: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
- return;
- }
- // parse message
- ArraySegment<byte> message = new ArraySegment<byte>(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($"KcpClient: 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<byte> 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();
- }
- }
- }
|