// call from transport update
 public void RawReceive()
 {
     try
     {
         if (socket != null)
         {
             while (socket.Poll(0, SelectMode.SelectRead))
             {
                 int msgLength = socket.ReceiveFrom(rawReceiveBuffer, ref remoteEndpoint);
                 // IMPORTANT: detect if buffer was too small for the
                 //            received msgLength. otherwise the excess
                 //            data would be silently lost.
                 //            (see ReceiveFrom documentation)
                 if (msgLength <= rawReceiveBuffer.Length)
                 {
                     //Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
                     RawInput(rawReceiveBuffer, msgLength);
                 }
                 else
                 {
                     KCPLog.Error($"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
                     Disconnect();
                 }
             }
         }
     }
     // this is fine, the socket might have been closed in the other end
     catch (SocketException) {}
 }
Пример #2
0
        public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
        {
            if (connected)
            {
                KCPLog.Warning("KCP: client already connected!");
                return;
            }

            connection = new KcpClientConnection();

            // setup events
            connection.OnAuthenticated = () =>
            {
                KCPLog.Info($"KCP: OnClientConnected");
                connected = true;
                OnConnected.Invoke();
            };
            connection.OnData = (message) =>
            {
                //Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
                OnData.Invoke(message);
            };
            connection.OnDisconnected = () =>
            {
                KCPLog.Info($"KCP: OnClientDisconnected");
                connected  = false;
                connection = null;
                OnDisconnected.Invoke();
            };

            // connect
            connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
        }
Пример #3
0
        void SendReliable(KcpHeader header, ArraySegment <byte> content)
        {
            // 1 byte header + content needs to fit into send buffer
            if (1 + content.Count <= kcpSendBuffer.Length) // TODO
            {
                // copy header, content (if any) into send buffer
                kcpSendBuffer[0] = (byte)header;
                if (content.Count > 0)
                {
                    Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count);
                }

                // send to kcp for processing
                int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count);
                if (sent < 0)
                {
                    KCPLog.Warning($"Send failed with error={sent} for content with length={content.Count}");
                }
            }
            // otherwise content is larger than MaxMessageSize. let user know!
            else
            {
                KCPLog.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize}");
            }
        }
Пример #4
0
 void HandleDeadLink()
 {
     // kcp has 'dead_link' detection. might as well use it.
     if (kcp.state == -1)
     {
         KCPLog.Warning("KCP Connection dead_link detected. Disconnecting.");
         Disconnect();
     }
 }
Пример #5
0
 void HandleTimeout(uint time)
 {
     // note: we are also sending a ping regularly, so timeout should
     //       only ever happen if the connection is truly gone.
     if (time >= lastReceiveTime + TIMEOUT)
     {
         KCPLog.Warning($"KCP: Connection timed out after not receiving any message for {TIMEOUT}ms. Disconnecting.");
         Disconnect();
     }
 }
Пример #6
0
 public void Send(ArraySegment <byte> segment, KcpChannel channel)
 {
     if (connected)
     {
         connection.SendData(segment, channel);
     }
     else
     {
         KCPLog.Warning("KCP: can't send because client not connected!");
     }
 }
Пример #7
0
 void SendUnreliable(ArraySegment <byte> message)
 {
     // message size needs to be <= unreliable max size
     if (message.Count <= UnreliableMaxMessageSize)
     {
         // copy channel header, data into raw send buffer, then send
         rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
         Buffer.BlockCopy(message.Array, 0, rawSendBuffer, 1, message.Count);
         RawSend(rawSendBuffer, message.Count + 1);
     }
     // otherwise content is larger than MaxMessageSize. let user know!
     else
     {
         KCPLog.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}");
     }
 }
Пример #8
0
        public void TickIncoming()
        {
            uint time = (uint)refTime.ElapsedMilliseconds;

            try
            {
                switch (state)
                {
                case KcpState.Connected:
                {
                    TickIncoming_Connected(time);
                    break;
                }

                case KcpState.Authenticated:
                {
                    TickIncoming_Authenticated(time);
                    break;
                }

                case KcpState.Disconnected:
                {
                    // do nothing while disconnected
                    break;
                }
                }
            }
            catch (SocketException exception)
            {
                // this is ok, the connection was closed
                KCPLog.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
                Disconnect();
            }
            catch (ObjectDisposedException exception)
            {
                // fine, socket was closed
                KCPLog.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
                Disconnect();
            }
            catch (Exception ex)
            {
                // unexpected
                KCPLog.Error(ex.ToString());
                Disconnect();
            }
        }
        public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
        {
            KCPLog.Info($"KcpClient: connect to {host}:{port}");
            IPAddress[] ipAddress = Dns.GetHostAddresses(host);
            if (ipAddress.Length < 1)
            {
                throw new SocketException((int)SocketError.HostNotFound);
            }

            remoteEndpoint = new IPEndPoint(ipAddress[0], port);
            socket         = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
            socket.Connect(remoteEndpoint);
            SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);

            // client should send handshake to server as very first message
            SendHandshake();

            RawReceive();
        }
Пример #10
0
        public void Start(ushort port)
        {
            // only start once
            if (socket != null)
            {
                KCPLog.Warning("KCP: server already started!");
            }

            // listen
#if UNITY_SWITCH
            // Switch does not support ipv6
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.Bind(new IPEndPoint(IPAddress.Any, port));
#else
            socket          = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
            socket.DualMode = true;
            socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
#endif
        }
Пример #11
0
        void TickIncoming_Connected(uint time)
        {
            // detect common events & ping
            HandleTimeout(time);
            HandleDeadLink();
            HandlePing(time);
            HandleChoked();

            // any reliable kcp message received?
            if (ReceiveNextReliable(out KcpHeader header, out ArraySegment <byte> message))
            {
                // message type FSM. no default so we never miss a case.
                switch (header)
                {
                case KcpHeader.Handshake:
                {
                    // we were waiting for a handshake.
                    // it proves that the other end speaks our protocol.
                    KCPLog.Info("KCP: received handshake");
                    state = KcpState.Authenticated;
                    OnAuthenticated?.Invoke();
                    break;
                }

                case KcpHeader.Ping:
                {
                    // ping keeps kcp from timing out. do nothing.
                    break;
                }

                case KcpHeader.Data:
                case KcpHeader.Disconnect:
                {
                    // everything else is not allowed during handshake!
                    KCPLog.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
                    Disconnect();
                    break;
                }
                }
            }
        }
Пример #12
0
        // reads the next reliable message type & content from kcp.
        // -> to avoid buffering, unreliable messages call OnData directly.
        bool ReceiveNextReliable(out KcpHeader header, out ArraySegment <byte> message)
        {
            header  = KcpHeader.Handshake;
            message = null;
            int msgSize = kcp.PeekSize();

            if (msgSize > 0)
            {
                // only allow receiving up to buffer sized messages.
                // otherwise we would get BlockCopy ArgumentException anyway.
                if (msgSize <= kcpMessageBuffer.Length)
                {
                    // receive from kcp
                    int received = kcp.Receive(kcpMessageBuffer, msgSize);
                    if (received >= 0)
                    {
                        // extract header & content without header
                        header          = (KcpHeader)kcpMessageBuffer[0];
                        message         = new ArraySegment <byte>(kcpMessageBuffer, 1, msgSize - 1);
                        lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
                        return(true);
                    }
                    else
                    {
                        // if receive failed, close everything
                        KCPLog.Warning($"Receive failed with error={received}. closing connection.");
                        Disconnect();
                    }
                }
                // we don't allow sending messages > Max, so this must be an
                // attacker. let's disconnect to avoid allocation attacks etc.
                else
                {
                    KCPLog.Warning($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
                    Disconnect();
                }
            }

            header = KcpHeader.Disconnect;
            return(false);
        }
Пример #13
0
        // disconnect this connection
        public void Disconnect()
        {
            // only if not disconnected yet
            if (state == KcpState.Disconnected)
            {
                return;
            }

            // send a disconnect message
            if (socket.Connected)
            {
                try
                {
                    SendDisconnect();
                    kcp.Flush();
                }
                catch (SocketException)
                {
                    // this is ok, the connection was already closed
                }
                catch (ObjectDisposedException)
                {
                    // this is normal when we stop the server
                    // the socket is stopped so we can't send anything anymore
                    // to the clients

                    // the clients will eventually timeout and realize they
                    // were disconnected
                }
            }

            // set as Disconnected, call event
            KCPLog.Info("KCP Connection: Disconnected.");
            state = KcpState.Disconnected;
            OnDisconnected?.Invoke();
        }
Пример #14
0
        public void SendData(ArraySegment <byte> data, KcpChannel channel)
        {
            // sending empty segments is not allowed.
            // nobody should ever try to send empty data.
            // it means that something went wrong, e.g. in Mirror/DOTSNET.
            // let's make it obvious so it's easy to debug.
            if (data.Count == 0)
            {
                KCPLog.Warning("KcpConnection: tried sending empty message. This should never happen. Disconnecting.");
                Disconnect();
                return;
            }

            switch (channel)
            {
            case KcpChannel.Reliable:
                SendReliable(KcpHeader.Data, data);
                break;

            case KcpChannel.Unreliable:
                SendUnreliable(data);
                break;
            }
        }
Пример #15
0
        void HandleChoked()
        {
            // disconnect connections that can't process the load.
            // see QueueSizeDisconnect comments.
            // => include all of kcp's buffers and the unreliable queue!
            int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
                        kcp.rcv_buf.Count + kcp.snd_buf.Count;

            if (total >= QueueDisconnectThreshold)
            {
                KCPLog.Warning($"KCP: disconnecting connection because it can't process data fast enough.\n" +
                               $"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
                               $"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
                               $"* Or perhaps the network is simply too slow on our end, or on the other end.\n");

                // let's clear all pending sends before disconnting with 'Bye'.
                // otherwise a single Flush in Disconnect() won't be enough to
                // flush thousands of messages to finally deliver 'Bye'.
                // this is just faster and more robust.
                kcp.snd_queue.Clear();

                Disconnect();
            }
        }
Пример #16
0
        public void RawInput(byte[] buffer, int msgLength)
        {
            // parse channel
            if (msgLength > 0)
            {
                byte channel = buffer[0];
                switch (channel)
                {
                case (byte)KcpChannel.Reliable:
                {
                    // input into kcp, but skip channel byte
                    int input = kcp.Input(buffer, 1, msgLength - 1);
                    if (input != 0)
                    {
                        KCPLog.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}");
                    }
                    break;
                }

                case (byte)KcpChannel.Unreliable:
                {
                    // ideally we would queue all unreliable messages and
                    // then process them in ReceiveNext() together with the
                    // reliable messages, but:
                    // -> queues/allocations/pools are slow and complex.
                    // -> DOTSNET 10k is actually slower if we use pooled
                    //    unreliable messages for transform messages.
                    //
                    //      DOTSNET 10k benchmark:
                    //        reliable-only:         170 FPS
                    //        unreliable queued: 130-150 FPS
                    //        unreliable direct:     183 FPS(!)
                    //
                    //      DOTSNET 50k benchmark:
                    //        reliable-only:         FAILS (queues keep growing)
                    //        unreliable direct:     18-22 FPS(!)
                    //
                    // -> all unreliable messages are DATA messages anyway.
                    // -> let's skip the magic and call OnData directly if
                    //    the current state allows it.
                    if (state == KcpState.Authenticated)
                    {
                        // only process messages while not paused for Mirror
                        // scene switching etc.
                        // -> if an unreliable message comes in while
                        //    paused, simply drop it. it's unreliable!
                        if (!paused)
                        {
                            ArraySegment <byte> message = new ArraySegment <byte>(buffer, 1, msgLength - 1);
                            OnData?.Invoke(message);
                        }

                        // set last receive time to avoid timeout.
                        // -> we do this in ANY case even if not enabled.
                        //    a message is a message.
                        // -> we set last receive time for both reliable and
                        //    unreliable messages. both count.
                        //    otherwise a connection might time out even
                        //    though unreliable were received, but no
                        //    reliable was received.
                        lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
                    }
                    else
                    {
                        // should never
                        KCPLog.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
                        Disconnect();
                    }
                    break;
                }

                default:
                {
                    // not a valid channel. random data or attacks.
                    KCPLog.Info($"Disconnecting connection because of invalid channel header: {channel}");
                    Disconnect();
                    break;
                }
                }
            }
        }
Пример #17
0
        public void TickIncoming()
        {
            while (socket != null && socket.Poll(0, SelectMode.SelectRead))
            {
                try
                {
                    int msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP);
                    //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");

                    // calculate connectionId from endpoint
                    int connectionId = newClientEP.GetHashCode();

                    // IMPORTANT: detect if buffer was too small for the received
                    //            msgLength. otherwise the excess data would be
                    //            silently lost.
                    //            (see ReceiveFrom documentation)
                    if (msgLength <= rawReceiveBuffer.Length)
                    {
                        // is this a new connection?
                        if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
                        {
                            // create a new KcpConnection
                            connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize);

                            // DO NOT add to connections yet. only if the first message
                            // is actually the kcp handshake. otherwise it's either:
                            // * random data from the internet
                            // * or from a client connection that we just disconnected
                            //   but that hasn't realized it yet, still sending data
                            //   from last session that we should absolutely ignore.
                            //
                            //
                            // TODO this allocates a new KcpConnection for each new
                            // internet connection. not ideal, but C# UDP Receive
                            // already allocated anyway.
                            //
                            // expecting a MAGIC byte[] would work, but sending the raw
                            // UDP message without kcp's reliability will have low
                            // probability of being received.
                            //
                            // for now, this is fine.

                            // setup authenticated event that also adds to connections
                            connection.OnAuthenticated = () =>
                            {
                                // only send handshake to client AFTER we received his
                                // handshake in OnAuthenticated.
                                // we don't want to reply to random internet messages
                                // with handshakes each time.
                                connection.SendHandshake();

                                // add to connections dict after being authenticated.
                                connections.Add(connectionId, connection);
                                KCPLog.Info($"KCP: server added connection({connectionId}): {newClientEP}");

                                // setup Data + Disconnected events only AFTER the
                                // handshake. we don't want to fire OnServerDisconnected
                                // every time we receive invalid random data from the
                                // internet.

                                // setup data event
                                connection.OnData = (message) =>
                                {
                                    // call mirror event
                                    //Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
                                    OnData.Invoke(connectionId, message);
                                };

                                // setup disconnected event
                                connection.OnDisconnected = () =>
                                {
                                    // flag for removal
                                    // (can't remove directly because connection is updated
                                    //  and event is called while iterating all connections)
                                    connectionsToRemove.Add(connectionId);

                                    // call mirror event
                                    KCPLog.Info($"KCP: OnServerDisconnected({connectionId})");
                                    OnDisconnected.Invoke(connectionId);
                                };

                                // finally, call mirror OnConnected event
                                KCPLog.Info($"KCP: OnServerConnected({connectionId})");
                                OnConnected.Invoke(connectionId);
                            };

                            // now input the message & process received ones
                            // connected event was set up.
                            // tick will process the first message and adds the
                            // connection if it was the handshake.
                            connection.RawInput(rawReceiveBuffer, msgLength);
                            connection.TickIncoming();

                            // again, do not add to connections.
                            // if the first message wasn't the kcp handshake then
                            // connection will simply be garbage collected.
                        }
                        // existing connection: simply input the message into kcp
                        else
                        {
                            connection.RawInput(rawReceiveBuffer, msgLength);
                        }
                    }
                    else
                    {
                        KCPLog.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}.");
                        Disconnect(connectionId);
                    }
                }
                // this is fine, the socket might have been closed in the other end
                catch (SocketException) {}
            }

            // process inputs for all server connections
            // (even if we didn't receive anything. need to tick ping etc.)
            foreach (KcpServerConnection connection in connections.Values)
            {
                connection.TickIncoming();
            }

            // remove disconnected connections
            // (can't do it in connection.OnDisconnected because Tick is called
            //  while iterating connections)
            foreach (int connectionId in connectionsToRemove)
            {
                connections.Remove(connectionId);
            }
            connectionsToRemove.Clear();
        }
Пример #18
0
 // server & client need to send handshake at different times, so we need
 // to expose the function.
 // * client should send it immediately.
 // * server should send it as reply to client's handshake, not before
 //   (server should not reply to random internet messages with handshake)
 // => handshake info needs to be delivered, so it goes over reliable.
 public void SendHandshake()
 {
     KCPLog.Info("KcpConnection: sending Handshake to other end!");
     SendReliable(KcpHeader.Handshake, default);
 }
Пример #19
0
        void TickIncoming_Authenticated(uint time)
        {
            // detect common events & ping
            HandleTimeout(time);
            HandleDeadLink();
            HandlePing(time);
            HandleChoked();

            // process all received messages
            //
            // Mirror scene changing requires transports to immediately stop
            // processing any more messages after a scene message was
            // received. and since we are in a while loop here, we need this
            // extra check.
            //
            // note while that this is mainly for Mirror, but might be
            // useful in other applications too.
            //
            // note that we check it BEFORE ever calling ReceiveNext. otherwise
            // we would silently eat the received message and never process it.
            while (!paused &&
                   ReceiveNextReliable(out KcpHeader header, out ArraySegment <byte> message))
            {
                // message type FSM. no default so we never miss a case.
                switch (header)
                {
                case KcpHeader.Handshake:
                {
                    // should never receive another handshake after auth
                    KCPLog.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection.");
                    Disconnect();
                    break;
                }

                case KcpHeader.Data:
                {
                    // call OnData IF the message contained actual data
                    if (message.Count > 0)
                    {
                        //Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}");
                        OnData?.Invoke(message);
                    }
                    // empty data = attacker, or something went wrong
                    else
                    {
                        KCPLog.Warning("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
                        Disconnect();
                    }
                    break;
                }

                case KcpHeader.Ping:
                {
                    // ping keeps kcp from timing out. do nothing.
                    break;
                }

                case KcpHeader.Disconnect:
                {
                    // disconnect might happen
                    KCPLog.Info("KCP: received disconnect message");
                    Disconnect();
                    break;
                }
                }
            }
        }