// 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) {} }
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); }
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}"); } }
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(); } }
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(); } }
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!"); } }
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}"); } }
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(); }
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 }
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; } } } }
// 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); }
// 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(); }
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; } }
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(); } }
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; } } } }
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(); }
// 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); }
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; } } } }