mirror of
				https://github.com/DerTyp7/defrain-shooter-unity.git
				synced 2025-10-31 13:37:08 +01:00 
			
		
		
		
	CHANGED TO MIRROR
This commit is contained in:
		| @@ -0,0 +1,10 @@ | ||||
| namespace kcp2k | ||||
| { | ||||
|     // channel type and header for raw messages | ||||
|     public enum KcpChannel : byte | ||||
|     { | ||||
|         // don't react on 0x00. might help to filter out random noise. | ||||
|         Reliable = 0x01, | ||||
|         Unreliable = 0x02 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 9e852b2532fb248d19715cfebe371db3 | ||||
| timeCreated: 1610081248 | ||||
							
								
								
									
										120
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| // kcp client logic abstracted into a class. | ||||
| // for use in Mirror, DOTSNET, testing, etc. | ||||
| using System; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpClient | ||||
|     { | ||||
|         // events | ||||
|         public Action OnConnected; | ||||
|         public Action<ArraySegment<byte>> OnData; | ||||
|         public Action OnDisconnected; | ||||
|  | ||||
|         // state | ||||
|         public KcpClientConnection connection; | ||||
|         public bool connected; | ||||
|  | ||||
|         public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected) | ||||
|         { | ||||
|             this.OnConnected = OnConnected; | ||||
|             this.OnData = OnData; | ||||
|             this.OnDisconnected = OnDisconnected; | ||||
|         } | ||||
|  | ||||
|         // CreateConnection can be overwritten for where-allocation: | ||||
|         // https://github.com/vis2k/where-allocation | ||||
|         protected virtual KcpClientConnection CreateConnection() => | ||||
|             new KcpClientConnection(); | ||||
|  | ||||
|         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, int timeout = KcpConnection.DEFAULT_TIMEOUT) | ||||
|         { | ||||
|             if (connected) | ||||
|             { | ||||
|                 Log.Warning("KCP: client already connected!"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // create connection | ||||
|             connection = CreateConnection(); | ||||
|  | ||||
|             // setup events | ||||
|             connection.OnAuthenticated = () => | ||||
|             { | ||||
|                 Log.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 = () => | ||||
|             { | ||||
|                 Log.Info($"KCP: OnClientDisconnected"); | ||||
|                 connected = false; | ||||
|                 connection = null; | ||||
|                 OnDisconnected.Invoke(); | ||||
|             }; | ||||
|  | ||||
|             // connect | ||||
|             connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); | ||||
|         } | ||||
|  | ||||
|         public void Send(ArraySegment<byte> segment, KcpChannel channel) | ||||
|         { | ||||
|             if (connected) | ||||
|             { | ||||
|                 connection.SendData(segment, channel); | ||||
|             } | ||||
|             else Log.Warning("KCP: can't send because client not connected!"); | ||||
|         } | ||||
|  | ||||
|         public void Disconnect() | ||||
|         { | ||||
|             // only if connected | ||||
|             // otherwise we end up in a deadlock because of an open Mirror bug: | ||||
|             // https://github.com/vis2k/Mirror/issues/2353 | ||||
|             if (connected) | ||||
|             { | ||||
|                 // call Disconnect and let the connection handle it. | ||||
|                 // DO NOT set it to null yet. it needs to be updated a few more | ||||
|                 // times first. let the connection handle it! | ||||
|                 connection?.Disconnect(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // process incoming messages. should be called before updating the world. | ||||
|         public 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) | ||||
|             connection?.RawReceive(); | ||||
|             connection?.TickIncoming(); | ||||
|         } | ||||
|  | ||||
|         // process outgoing messages. should be called after updating the world. | ||||
|         public void TickOutgoing() | ||||
|         { | ||||
|             // process outgoing | ||||
|             // (connection is null if not active) | ||||
|             connection?.TickOutgoing(); | ||||
|         } | ||||
|  | ||||
|         // process incoming and outgoing for convenience | ||||
|         // => ideally call ProcessIncoming() before updating the world and | ||||
|         //    ProcessOutgoing() after updating the world for minimum latency | ||||
|         public void Tick() | ||||
|         { | ||||
|             TickIncoming(); | ||||
|             TickOutgoing(); | ||||
|         } | ||||
|  | ||||
|         // pause/unpause to safely support mirror scene handling and to | ||||
|         // immediately pause the receive while loop if needed. | ||||
|         public void Pause() => connection?.Pause(); | ||||
|         public void Unpause() => connection?.Unpause(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 6aa069a28ed24fedb533c102d9742b36 | ||||
| timeCreated: 1603786960 | ||||
| @@ -0,0 +1,109 @@ | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpClientConnection : KcpConnection | ||||
|     { | ||||
|         // IMPORTANT: 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! | ||||
|         readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; | ||||
|  | ||||
|         // helper function to resolve host to IPAddress | ||||
|         public static bool ResolveHostname(string hostname, out IPAddress[] addresses) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 addresses = Dns.GetHostAddresses(hostname); | ||||
|                 return addresses.Length >= 1; | ||||
|             } | ||||
|             catch (SocketException) | ||||
|             { | ||||
|                 Log.Info($"Failed to resolve host: {hostname}"); | ||||
|                 addresses = null; | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // EndPoint & Receive functions can be overwritten for where-allocation: | ||||
|         // https://github.com/vis2k/where-allocation | ||||
|         // NOTE: Client's SendTo doesn't allocate, don't need a virtual. | ||||
|         protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) => | ||||
|             remoteEndPoint = new IPEndPoint(addresses[0], port); | ||||
|  | ||||
|         protected virtual int ReceiveFrom(byte[] buffer) => | ||||
|             socket.ReceiveFrom(buffer, ref remoteEndPoint); | ||||
|  | ||||
|         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, int timeout = DEFAULT_TIMEOUT) | ||||
|         { | ||||
|             Log.Info($"KcpClient: connect to {host}:{port}"); | ||||
|  | ||||
|             // try resolve host name | ||||
|             if (ResolveHostname(host, out IPAddress[] addresses)) | ||||
|             { | ||||
|                 // create remote endpoint | ||||
|                 CreateRemoteEndPoint(addresses, port); | ||||
|  | ||||
|                 // create socket | ||||
|                 socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); | ||||
|                 socket.Connect(remoteEndPoint); | ||||
|  | ||||
|                 // set up kcp | ||||
|                 SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); | ||||
|  | ||||
|                 // client should send handshake to server as very first message | ||||
|                 SendHandshake(); | ||||
|  | ||||
|                 RawReceive(); | ||||
|             } | ||||
|             // otherwise call OnDisconnected to let the user know. | ||||
|             else OnDisconnected(); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         // call from transport update | ||||
|         public void RawReceive() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 if (socket != null) | ||||
|                 { | ||||
|                     while (socket.Poll(0, SelectMode.SelectRead)) | ||||
|                     { | ||||
|                         int msgLength = ReceiveFrom(rawReceiveBuffer); | ||||
|                         // 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 | ||||
|                         { | ||||
|                             Log.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) {} | ||||
|         } | ||||
|  | ||||
|         protected override void Dispose() | ||||
|         { | ||||
|             socket.Close(); | ||||
|             socket = null; | ||||
|         } | ||||
|  | ||||
|         protected override void RawSend(byte[] data, int length) | ||||
|         { | ||||
|             socket.Send(data, length, SocketFlags.None); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 96512e74aa8214a6faa8a412a7a07877 | ||||
| timeCreated: 1602601237 | ||||
| @@ -0,0 +1,674 @@ | ||||
| using System; | ||||
| using System.Diagnostics; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     enum KcpState { Connected, Authenticated, Disconnected } | ||||
|  | ||||
|     public abstract class KcpConnection | ||||
|     { | ||||
|         protected Socket socket; | ||||
|         protected EndPoint remoteEndPoint; | ||||
|         internal Kcp kcp; | ||||
|  | ||||
|         // kcp can have several different states, let's use a state machine | ||||
|         KcpState state = KcpState.Disconnected; | ||||
|  | ||||
|         public Action OnAuthenticated; | ||||
|         public Action<ArraySegment<byte>> OnData; | ||||
|         public Action OnDisconnected; | ||||
|  | ||||
|         // Mirror needs a way to stop the kcp message processing while loop | ||||
|         // immediately after a scene change message. Mirror can't process any | ||||
|         // other messages during a scene change. | ||||
|         // (could be useful for others too) | ||||
|         bool paused; | ||||
|  | ||||
|         // If we don't receive anything these many milliseconds | ||||
|         // then consider us disconnected | ||||
|         public const int DEFAULT_TIMEOUT = 10000; | ||||
|         public int timeout = DEFAULT_TIMEOUT; | ||||
|         uint lastReceiveTime; | ||||
|  | ||||
|         // internal time. | ||||
|         // StopWatch offers ElapsedMilliSeconds and should be more precise than | ||||
|         // Unity's time.deltaTime over long periods. | ||||
|         readonly Stopwatch refTime = new Stopwatch(); | ||||
|  | ||||
|         // we need to subtract the channel byte from every MaxMessageSize | ||||
|         // calculation. | ||||
|         // we also need to tell kcp to use MTU-1 to leave space for the byte. | ||||
|         const int CHANNEL_HEADER_SIZE = 1; | ||||
|  | ||||
|         // reliable channel (= kcp) MaxMessageSize so the outside knows largest | ||||
|         // allowed message to send the calculation in Send() is not obvious at | ||||
|         // all, so let's provide the helper here. | ||||
|         // | ||||
|         // kcp does fragmentation, so max message is way larger than MTU. | ||||
|         // | ||||
|         // -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD | ||||
|         // -> Send() checks if fragment count < WND_RCV, so we use WND_RCV - 1. | ||||
|         //    note that Send() checks WND_RCV instead of wnd_rcv which may or | ||||
|         //    may not be a bug in original kcp. but since it uses the define, we | ||||
|         //    can use that here too. | ||||
|         // -> we add 1 byte KcpHeader enum to each message, so -1 | ||||
|         // | ||||
|         // IMPORTANT: max message is MTU * WND_RCV, in other words it completely | ||||
|         //            fills the receive window! due to head of line blocking, | ||||
|         //            all other messages have to wait while a maxed size message | ||||
|         //            is being delivered. | ||||
|         //            => in other words, DO NOT use max size all the time like | ||||
|         //               for batching. | ||||
|         //            => sending UNRELIABLE max message size most of the time is | ||||
|         //               best for performance (use that one for batching!) | ||||
|         public const int ReliableMaxMessageSize = (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * (Kcp.WND_RCV - 1) - 1; | ||||
|  | ||||
|         // unreliable max message size is simply MTU - channel header size | ||||
|         public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE; | ||||
|  | ||||
|         // buffer to receive kcp's processed messages (avoids allocations). | ||||
|         // IMPORTANT: this is for KCP messages. so it needs to be of size: | ||||
|         //            1 byte header + MaxMessageSize content | ||||
|         byte[] kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize]; | ||||
|  | ||||
|         // send buffer for handing user messages to kcp for processing. | ||||
|         // (avoids allocations). | ||||
|         // IMPORTANT: needs to be of size: | ||||
|         //            1 byte header + MaxMessageSize content | ||||
|         byte[] kcpSendBuffer = new byte[1 + ReliableMaxMessageSize]; | ||||
|  | ||||
|         // raw send buffer is exactly MTU. | ||||
|         byte[] rawSendBuffer = new byte[Kcp.MTU_DEF]; | ||||
|  | ||||
|         // send a ping occasionally so we don't time out on the other end. | ||||
|         // for example, creating a character in an MMO could easily take a | ||||
|         // minute of no data being sent. which doesn't mean we want to time out. | ||||
|         // same goes for slow paced card games etc. | ||||
|         public const int PING_INTERVAL = 1000; | ||||
|         uint lastPingTime; | ||||
|  | ||||
|         // if we send more than kcp can handle, we will get ever growing | ||||
|         // send/recv buffers and queues and minutes of latency. | ||||
|         // => if a connection can't keep up, it should be disconnected instead | ||||
|         //    to protect the server under heavy load, and because there is no | ||||
|         //    point in growing to gigabytes of memory or minutes of latency! | ||||
|         // => 2k isn't enough. we reach 2k when spawning 4k monsters at once | ||||
|         //    easily, but it does recover over time. | ||||
|         // => 10k seems safe. | ||||
|         // | ||||
|         // note: we have a ChokeConnectionAutoDisconnects test for this too! | ||||
|         internal const int QueueDisconnectThreshold = 10000; | ||||
|  | ||||
|         // getters for queue and buffer counts, used for debug info | ||||
|         public int SendQueueCount => kcp.snd_queue.Count; | ||||
|         public int ReceiveQueueCount => kcp.rcv_queue.Count; | ||||
|         public int SendBufferCount => kcp.snd_buf.Count; | ||||
|         public int ReceiveBufferCount => kcp.rcv_buf.Count; | ||||
|  | ||||
|         // maximum send rate per second can be calculated from kcp parameters | ||||
|         // source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html | ||||
|         // | ||||
|         // KCP can send/receive a maximum of WND*MTU per interval. | ||||
|         // multiple by 1000ms / interval to get the per-second rate. | ||||
|         // | ||||
|         // example: | ||||
|         //   WND(32) * MTU(1400) = 43.75KB | ||||
|         //   => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s | ||||
|         // | ||||
|         // returns bytes/second! | ||||
|         public uint MaxSendRate => | ||||
|             kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval; | ||||
|  | ||||
|         public uint MaxReceiveRate => | ||||
|             kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval; | ||||
|  | ||||
|         // SetupKcp creates and configures a new KCP instance. | ||||
|         // => useful to start from a fresh state every time the client connects | ||||
|         // => NoDelay, interval, wnd size are the most important configurations. | ||||
|         //    let's force require the parameters so we don't forget it anywhere. | ||||
|         protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) | ||||
|         { | ||||
|             // set up kcp over reliable channel (that's what kcp is for) | ||||
|             kcp = new Kcp(0, RawSendReliable); | ||||
|             // set nodelay. | ||||
|             // note that kcp uses 'nocwnd' internally so we negate the parameter | ||||
|             kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow); | ||||
|             kcp.SetWindowSize(sendWindowSize, receiveWindowSize); | ||||
|  | ||||
|             // IMPORTANT: high level needs to add 1 channel byte to each raw | ||||
|             // message. so while Kcp.MTU_DEF is perfect, we actually need to | ||||
|             // tell kcp to use MTU-1 so we can still put the header into the | ||||
|             // message afterwards. | ||||
|             kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE); | ||||
|  | ||||
|             this.timeout = timeout; | ||||
|             state = KcpState.Connected; | ||||
|  | ||||
|             refTime.Start(); | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|             { | ||||
|                 Log.Warning($"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         void HandleDeadLink() | ||||
|         { | ||||
|             // kcp has 'dead_link' detection. might as well use it. | ||||
|             if (kcp.state == -1) | ||||
|             { | ||||
|                 Log.Warning("KCP Connection dead_link detected. Disconnecting."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // send a ping occasionally in order to not time out on the other end. | ||||
|         void HandlePing(uint time) | ||||
|         { | ||||
|             // enough time elapsed since last ping? | ||||
|             if (time >= lastPingTime + PING_INTERVAL) | ||||
|             { | ||||
|                 // ping again and reset time | ||||
|                 //Log.Debug("KCP: sending ping..."); | ||||
|                 SendPing(); | ||||
|                 lastPingTime = time; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|             { | ||||
|                 Log.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(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 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) | ||||
|         { | ||||
|             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 | ||||
|                         Log.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 | ||||
|                 { | ||||
|                     Log.Warning($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); | ||||
|                     Disconnect(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             message = default; | ||||
|             header = KcpHeader.Disconnect; | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         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. | ||||
|                         Log.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! | ||||
|                         Log.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection."); | ||||
|                         Disconnect(); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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 | ||||
|                         Log.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 | ||||
|                         { | ||||
|                             Log.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 | ||||
|                         Log.Info("KCP: received disconnect message"); | ||||
|                         Disconnect(); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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 | ||||
|                 Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|             catch (ObjectDisposedException exception) | ||||
|             { | ||||
|                 // fine, socket was closed | ||||
|                 Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 // unexpected | ||||
|                 Log.Error(ex.ToString()); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void TickOutgoing() | ||||
|         { | ||||
|             uint time = (uint)refTime.ElapsedMilliseconds; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 switch (state) | ||||
|                 { | ||||
|                     case KcpState.Connected: | ||||
|                     case KcpState.Authenticated: | ||||
|                     { | ||||
|                         // update flushes out messages | ||||
|                         kcp.Update(time); | ||||
|                         break; | ||||
|                     } | ||||
|                     case KcpState.Disconnected: | ||||
|                     { | ||||
|                         // do nothing while disconnected | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             catch (SocketException exception) | ||||
|             { | ||||
|                 // this is ok, the connection was closed | ||||
|                 Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|             catch (ObjectDisposedException exception) | ||||
|             { | ||||
|                 // fine, socket was closed | ||||
|                 Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine."); | ||||
|                 Disconnect(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 // unexpected | ||||
|                 Log.Error(ex.ToString()); | ||||
|                 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) | ||||
|                         { | ||||
|                             Log.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 | ||||
|                             Log.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection."); | ||||
|                             Disconnect(); | ||||
|                         } | ||||
|                         break; | ||||
|                     } | ||||
|                     default: | ||||
|                     { | ||||
|                         // not a valid channel. random data or attacks. | ||||
|                         Log.Info($"Disconnecting connection because of invalid channel header: {channel}"); | ||||
|                         Disconnect(); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // raw send puts the data into the socket | ||||
|         protected abstract void RawSend(byte[] data, int length); | ||||
|  | ||||
|         // raw send called by kcp | ||||
|         void RawSendReliable(byte[] data, int length) | ||||
|         { | ||||
|             // copy channel header, data into raw send buffer, then send | ||||
|             rawSendBuffer[0] = (byte)KcpChannel.Reliable; | ||||
|             Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length); | ||||
|             RawSend(rawSendBuffer, length + 1); | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|                 { | ||||
|                     Log.Warning($"Send failed with error={sent} for content with length={content.Count}"); | ||||
|                 } | ||||
|             } | ||||
|             // otherwise content is larger than MaxMessageSize. let user know! | ||||
|             else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize}"); | ||||
|         } | ||||
|  | ||||
|         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 Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}"); | ||||
|         } | ||||
|  | ||||
|         // 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() | ||||
|         { | ||||
|             Log.Info("KcpConnection: sending Handshake to other end!"); | ||||
|             SendReliable(KcpHeader.Handshake, default); | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|             { | ||||
|                 Log.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; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // ping goes through kcp to keep it from timing out, so it goes over the | ||||
|         // reliable channel. | ||||
|         void SendPing() => SendReliable(KcpHeader.Ping, default); | ||||
|  | ||||
|         // disconnect info needs to be delivered, so it goes over reliable | ||||
|         void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default); | ||||
|  | ||||
|         protected virtual void Dispose() {} | ||||
|  | ||||
|         // 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 | ||||
|             Log.Info("KCP Connection: Disconnected."); | ||||
|             state = KcpState.Disconnected; | ||||
|             OnDisconnected?.Invoke(); | ||||
|         } | ||||
|  | ||||
|         // get remote endpoint | ||||
|         public EndPoint GetRemoteEndPoint() => remoteEndPoint; | ||||
|  | ||||
|         // pause/unpause to safely support mirror scene handling and to | ||||
|         // immediately pause the receive while loop if needed. | ||||
|         public void Pause() => paused = true; | ||||
|         public void Unpause() | ||||
|         { | ||||
|             // unpause | ||||
|             paused = false; | ||||
|  | ||||
|             // reset the timeout. | ||||
|             // we have likely been paused for > timeout seconds, but that | ||||
|             // doesn't mean we should disconnect. for example, Mirror pauses | ||||
|             // kcp during scene changes which could easily take > 10s timeout: | ||||
|             //   see also: https://github.com/vis2k/kcp2k/issues/8 | ||||
|             // => Unpause completely resets the timeout instead of restoring the | ||||
|             //    time difference when we started pausing. it's more simple and | ||||
|             //    it's a good idea to start counting from 0 after we unpaused! | ||||
|             lastReceiveTime = (uint)refTime.ElapsedMilliseconds; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 3915c7c62b72d4dc2a9e4e76c94fc484 | ||||
| timeCreated: 1602600432 | ||||
| @@ -0,0 +1,19 @@ | ||||
| namespace kcp2k | ||||
| { | ||||
|     // header for messages processed by kcp. | ||||
|     // this is NOT for the raw receive messages(!) because handshake/disconnect | ||||
|     // need to be sent reliably. it's not enough to have those in rawreceive | ||||
|     // because those messages might get lost without being resent! | ||||
|     public enum KcpHeader : byte | ||||
|     { | ||||
|         // don't react on 0x00. might help to filter out random noise. | ||||
|         Handshake = 0x01, | ||||
|         // ping goes over reliable & KcpHeader for now. could go over reliable | ||||
|         // too. there is no real difference except that this is easier because | ||||
|         // we already have a KcpHeader for reliable messages. | ||||
|         // ping is only used to keep it alive, so latency doesn't matter. | ||||
|         Ping = 0x02, | ||||
|         Data = 0x03, | ||||
|         Disconnect = 0x04 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 91b5edac31224a49bd76f960ae018942 | ||||
| timeCreated: 1610081248 | ||||
							
								
								
									
										337
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,337 @@ | ||||
| // kcp server logic abstracted into a class. | ||||
| // for use in Mirror, DOTSNET, testing, etc. | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpServer | ||||
|     { | ||||
|         // events | ||||
|         public Action<int> OnConnected; | ||||
|         public Action<int, ArraySegment<byte>> OnData; | ||||
|         public Action<int> OnDisconnected; | ||||
|  | ||||
|         // configuration | ||||
|         // DualMode uses both IPv6 and IPv4. not all platforms support it. | ||||
|         // (Nintendo Switch, etc.) | ||||
|         public bool DualMode; | ||||
|         // NoDelay is recommended to reduce latency. This also scales better | ||||
|         // without buffers getting full. | ||||
|         public bool NoDelay; | ||||
|         // KCP internal update interval. 100ms is KCP default, but a lower | ||||
|         // interval is recommended to minimize latency and to scale to more | ||||
|         // networked entities. | ||||
|         public uint Interval; | ||||
|         // KCP fastresend parameter. Faster resend for the cost of higher | ||||
|         // bandwidth. | ||||
|         public int FastResend; | ||||
|         // KCP 'NoCongestionWindow' is false by default. here we negate it for | ||||
|         // ease of use. This can be disabled for high scale games if connections | ||||
|         // choke regularly. | ||||
|         public bool CongestionWindow; | ||||
|         // KCP window size can be modified to support higher loads. | ||||
|         // for example, Mirror Benchmark requires: | ||||
|         //   128, 128 for 4k monsters | ||||
|         //   512, 512 for 10k monsters | ||||
|         //  8192, 8192 for 20k monsters | ||||
|         public uint SendWindowSize; | ||||
|         public uint ReceiveWindowSize; | ||||
|         // timeout in milliseconds | ||||
|         public int Timeout; | ||||
|  | ||||
|         // state | ||||
|         protected Socket socket; | ||||
|         EndPoint newClientEP; | ||||
|  | ||||
|         // IMPORTANT: 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! | ||||
|         readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; | ||||
|  | ||||
|         // connections <connectionId, connection> where connectionId is EndPoint.GetHashCode | ||||
|         public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>(); | ||||
|  | ||||
|         public KcpServer(Action<int> OnConnected, | ||||
|                          Action<int, ArraySegment<byte>> OnData, | ||||
|                          Action<int> OnDisconnected, | ||||
|                          bool DualMode, | ||||
|                          bool NoDelay, | ||||
|                          uint Interval, | ||||
|                          int FastResend = 0, | ||||
|                          bool CongestionWindow = true, | ||||
|                          uint SendWindowSize = Kcp.WND_SND, | ||||
|                          uint ReceiveWindowSize = Kcp.WND_RCV, | ||||
|                          int Timeout = KcpConnection.DEFAULT_TIMEOUT) | ||||
|         { | ||||
|             this.OnConnected = OnConnected; | ||||
|             this.OnData = OnData; | ||||
|             this.OnDisconnected = OnDisconnected; | ||||
|             this.DualMode = DualMode; | ||||
|             this.NoDelay = NoDelay; | ||||
|             this.Interval = Interval; | ||||
|             this.FastResend = FastResend; | ||||
|             this.CongestionWindow = CongestionWindow; | ||||
|             this.SendWindowSize = SendWindowSize; | ||||
|             this.ReceiveWindowSize = ReceiveWindowSize; | ||||
|             this.Timeout = Timeout; | ||||
|  | ||||
|             // create newClientEP either IPv4 or IPv6 | ||||
|             newClientEP = DualMode | ||||
|                           ? new IPEndPoint(IPAddress.IPv6Any, 0) | ||||
|                           : new IPEndPoint(IPAddress.Any, 0); | ||||
|         } | ||||
|  | ||||
|         public bool IsActive() => socket != null; | ||||
|  | ||||
|         public void Start(ushort port) | ||||
|         { | ||||
|             // only start once | ||||
|             if (socket != null) | ||||
|             { | ||||
|                 Log.Warning("KCP: server already started!"); | ||||
|             } | ||||
|  | ||||
|             // listen | ||||
|             if (DualMode) | ||||
|             { | ||||
|                 // IPv6 socket with DualMode | ||||
|                 socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); | ||||
|                 socket.DualMode = true; | ||||
|                 socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // IPv4 socket | ||||
|                 socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); | ||||
|                 socket.Bind(new IPEndPoint(IPAddress.Any, port)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel) | ||||
|         { | ||||
|             if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) | ||||
|             { | ||||
|                 connection.SendData(segment, channel); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Disconnect(int connectionId) | ||||
|         { | ||||
|             if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) | ||||
|             { | ||||
|                 connection.Disconnect(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public string GetClientAddress(int connectionId) | ||||
|         { | ||||
|             if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) | ||||
|             { | ||||
|                 return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString(); | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         // EndPoint & Receive functions can be overwritten for where-allocation: | ||||
|         // https://github.com/vis2k/where-allocation | ||||
|         protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash) | ||||
|         { | ||||
|             // NOTE: ReceiveFrom allocates. | ||||
|             //   we pass our IPEndPoint to ReceiveFrom. | ||||
|             //   receive from calls newClientEP.Create(socketAddr). | ||||
|             //   IPEndPoint.Create always returns a new IPEndPoint. | ||||
|             //   https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 | ||||
|             int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP); | ||||
|  | ||||
|             // calculate connectionHash from endpoint | ||||
|             // NOTE: IPEndPoint.GetHashCode() allocates. | ||||
|             //  it calls m_Address.GetHashCode(). | ||||
|             //  m_Address is an IPAddress. | ||||
|             //  GetHashCode() allocates for IPv6: | ||||
|             //  https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 | ||||
|             // | ||||
|             // => using only newClientEP.Port wouldn't work, because | ||||
|             //    different connections can have the same port. | ||||
|             connectionHash = newClientEP.GetHashCode(); | ||||
|             return read; | ||||
|         } | ||||
|  | ||||
|         protected virtual KcpServerConnection CreateConnection() => | ||||
|             new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); | ||||
|  | ||||
|         // process incoming messages. should be called before updating the world. | ||||
|         HashSet<int> connectionsToRemove = new HashSet<int>(); | ||||
|         public void TickIncoming() | ||||
|         { | ||||
|             while (socket != null && socket.Poll(0, SelectMode.SelectRead)) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     // receive | ||||
|                     int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId); | ||||
|                     //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); | ||||
|  | ||||
|                     // 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 based on last received | ||||
|                             // EndPoint. can be overwritten for where-allocation. | ||||
|                             connection = CreateConnection(); | ||||
|  | ||||
|                             // 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); | ||||
|                                 Log.Info($"KCP: server added connection({connectionId})"); | ||||
|  | ||||
|                                 // 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 | ||||
|                                     Log.Info($"KCP: OnServerDisconnected({connectionId})"); | ||||
|                                     OnDisconnected.Invoke(connectionId); | ||||
|                                 }; | ||||
|  | ||||
|                                 // finally, call mirror OnConnected event | ||||
|                                 Log.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 | ||||
|                     { | ||||
|                         Log.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(); | ||||
|         } | ||||
|  | ||||
|         // process outgoing messages. should be called after updating the world. | ||||
|         public void TickOutgoing() | ||||
|         { | ||||
|             // flush all server connections | ||||
|             foreach (KcpServerConnection connection in connections.Values) | ||||
|             { | ||||
|                 connection.TickOutgoing(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // process incoming and outgoing for convenience. | ||||
|         // => ideally call ProcessIncoming() before updating the world and | ||||
|         //    ProcessOutgoing() after updating the world for minimum latency | ||||
|         public void Tick() | ||||
|         { | ||||
|             TickIncoming(); | ||||
|             TickOutgoing(); | ||||
|         } | ||||
|  | ||||
|         public void Stop() | ||||
|         { | ||||
|             socket?.Close(); | ||||
|             socket = null; | ||||
|         } | ||||
|  | ||||
|         // pause/unpause to safely support mirror scene handling and to | ||||
|         // immediately pause the receive while loop if needed. | ||||
|         public void Pause() | ||||
|         { | ||||
|             foreach (KcpServerConnection connection in connections.Values) | ||||
|                 connection.Pause(); | ||||
|         } | ||||
|  | ||||
|         public void Unpause() | ||||
|         { | ||||
|             foreach (KcpServerConnection connection in connections.Values) | ||||
|                 connection.Unpause(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 9759159c6589494a9037f5e130a867ed | ||||
| timeCreated: 1603787747 | ||||
| @@ -0,0 +1,22 @@ | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpServerConnection : KcpConnection | ||||
|     { | ||||
|         // Constructor & Send functions can be overwritten for where-allocation: | ||||
|         // https://github.com/vis2k/where-allocation | ||||
|         public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) | ||||
|         { | ||||
|             this.socket = socket; | ||||
|             this.remoteEndPoint = remoteEndPoint; | ||||
|             SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); | ||||
|         } | ||||
|  | ||||
|         protected override void RawSend(byte[] data, int length) | ||||
|         { | ||||
|             socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 80a9b1ce9a6f14abeb32bfa9921d097b | ||||
| timeCreated: 1602601483 | ||||
							
								
								
									
										14
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // A simple logger class that uses Console.WriteLine by default. | ||||
| // Can also do Logger.LogMethod = Debug.Log for Unity etc. | ||||
| // (this way we don't have to depend on UnityEngine) | ||||
| using System; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public static class Log | ||||
|     { | ||||
|         public static Action<string> Info = Console.WriteLine; | ||||
|         public static Action<string> Warning = Console.WriteLine; | ||||
|         public static Action<string> Error = Console.Error.WriteLine; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 7b5e1de98d6d84c3793a61cf7d8da9a4 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 0b320ff06046474eae7bce7240ea478c | ||||
| timeCreated: 1626430641 | ||||
| @@ -0,0 +1,24 @@ | ||||
| // where-allocation version of KcpClientConnection. | ||||
| // may not be wanted on all platforms, so it's an extra optional class. | ||||
| using System.Net; | ||||
| using WhereAllocation; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpClientConnectionNonAlloc : KcpClientConnection | ||||
|     { | ||||
|         IPEndPointNonAlloc reusableEP; | ||||
|  | ||||
|         protected override void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) | ||||
|         { | ||||
|             // create reusableEP with same address family as remoteEndPoint. | ||||
|             // otherwise ReceiveFrom_NonAlloc couldn't use it. | ||||
|             reusableEP = new IPEndPointNonAlloc(addresses[0], port); | ||||
|             base.CreateRemoteEndPoint(addresses, port); | ||||
|         } | ||||
|  | ||||
|         // where-allocation nonalloc recv | ||||
|         protected override int ReceiveFrom(byte[] buffer) => | ||||
|             socket.ReceiveFrom_NonAlloc(buffer, reusableEP); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 4c1b235bbe054706bef6d092f361006e | ||||
| timeCreated: 1626430539 | ||||
| @@ -0,0 +1,17 @@ | ||||
| // where-allocation version of KcpClientConnectionNonAlloc. | ||||
| // may not be wanted on all platforms, so it's an extra optional class. | ||||
| using System; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpClientNonAlloc : KcpClient | ||||
|     { | ||||
|         public KcpClientNonAlloc(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected) | ||||
|             : base(OnConnected, OnData, OnDisconnected) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         protected override KcpClientConnection CreateConnection() => | ||||
|             new KcpClientConnectionNonAlloc(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 2cf0ccf7d551480bb5af08fcbe169f84 | ||||
| timeCreated: 1626435264 | ||||
| @@ -0,0 +1,25 @@ | ||||
| // where-allocation version of KcpServerConnection. | ||||
| // may not be wanted on all platforms, so it's an extra optional class. | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using WhereAllocation; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpServerConnectionNonAlloc : KcpServerConnection | ||||
|     { | ||||
|         IPEndPointNonAlloc reusableSendEndPoint; | ||||
|  | ||||
|         public KcpServerConnectionNonAlloc(Socket socket, EndPoint remoteEndpoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) | ||||
|             : base(socket, remoteEndpoint, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout) | ||||
|         { | ||||
|             this.reusableSendEndPoint = reusableSendEndPoint; | ||||
|         } | ||||
|  | ||||
|         protected override void RawSend(byte[] data, int length) | ||||
|         { | ||||
|             // where-allocation nonalloc send | ||||
|             socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 4e1b74cc224b4c83a0f6c8d8da9090ab | ||||
| timeCreated: 1626430608 | ||||
| @@ -0,0 +1,51 @@ | ||||
| // where-allocation version of KcpServer. | ||||
| // may not be wanted on all platforms, so it's an extra optional class. | ||||
| using System; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using WhereAllocation; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class KcpServerNonAlloc : KcpServer | ||||
|     { | ||||
|         IPEndPointNonAlloc reusableClientEP; | ||||
|  | ||||
|         public KcpServerNonAlloc(Action<int> OnConnected, Action<int, ArraySegment<byte>> OnData, Action<int> OnDisconnected, bool DualMode, bool NoDelay, uint Interval, int FastResend = 0, bool CongestionWindow = true, uint SendWindowSize = Kcp.WND_SND, uint ReceiveWindowSize = Kcp.WND_RCV, int Timeout = KcpConnection.DEFAULT_TIMEOUT) | ||||
|             : base(OnConnected, OnData, OnDisconnected, DualMode, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout) | ||||
|         { | ||||
|             // create reusableClientEP either IPv4 or IPv6 | ||||
|             reusableClientEP = DualMode | ||||
|                 ? new IPEndPointNonAlloc(IPAddress.IPv6Any, 0) | ||||
|                 : new IPEndPointNonAlloc(IPAddress.Any, 0); | ||||
|         } | ||||
|  | ||||
|         protected override int ReceiveFrom(byte[] buffer, out int connectionHash) | ||||
|         { | ||||
|             // where-allocation nonalloc ReceiveFrom. | ||||
|             int read = socket.ReceiveFrom_NonAlloc(buffer, 0, buffer.Length, SocketFlags.None, reusableClientEP); | ||||
|             SocketAddress remoteAddress = reusableClientEP.temp; | ||||
|  | ||||
|             // where-allocation nonalloc GetHashCode | ||||
|             connectionHash = remoteAddress.GetHashCode(); | ||||
|             return read; | ||||
|         } | ||||
|  | ||||
|         protected override KcpServerConnection CreateConnection() | ||||
|         { | ||||
|             // IPEndPointNonAlloc is reused all the time. | ||||
|             // we can't store that as the connection's endpoint. | ||||
|             // we need a new copy! | ||||
|             IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint(); | ||||
|  | ||||
|             // for allocation free sending, we also need another | ||||
|             // IPEndPointNonAlloc... | ||||
|             IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port); | ||||
|  | ||||
|             // create a new KcpConnection NonAlloc version | ||||
|             // -> where-allocation IPEndPointNonAlloc is reused. | ||||
|             //    need to create a new one from the temp address. | ||||
|             return new KcpServerConnectionNonAlloc(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 54b8398dcd544c8a93bcad846214cc40 | ||||
| timeCreated: 1626432191 | ||||
		Reference in New Issue
	
	Block a user
	 DerTyp187
					DerTyp187