mirror of
				https://github.com/DerTyp7/defrain-shooter-unity.git
				synced 2025-10-30 21:17:09 +01:00 
			
		
		
		
	CHANGED TO MIRROR
This commit is contained in:
		
							
								
								
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 7bdb797750d0a490684410110bf48192 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,346 @@ | ||||
| //#if MIRROR <- commented out because MIRROR isn't defined on first import yet | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Net; | ||||
| using UnityEngine; | ||||
| using Mirror; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] | ||||
|     [DisallowMultipleComponent] | ||||
|     public class KcpTransport : Transport | ||||
|     { | ||||
|         // scheme used by this transport | ||||
|         public const string Scheme = "kcp"; | ||||
|  | ||||
|         // common | ||||
|         [Header("Transport Configuration")] | ||||
|         public ushort Port = 7777; | ||||
|         [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] | ||||
|         public bool DualMode = true; | ||||
|         [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] | ||||
|         public bool NoDelay = true; | ||||
|         [Tooltip("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 = 10; | ||||
|         [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] | ||||
|         public int Timeout = 10000; | ||||
|  | ||||
|         [Header("Advanced")] | ||||
|         [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] | ||||
|         public int FastResend = 2; | ||||
|         [Tooltip("KCP congestion window. Enabled in normal mode, disabled in turbo mode. Disable this for high scale games if connections get choked regularly.")] | ||||
|         public bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use. | ||||
|         [Tooltip("KCP window size can be modified to support higher loads.")] | ||||
|         public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. | ||||
|         [Tooltip("KCP window size can be modified to support higher loads.")] | ||||
|         public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more. | ||||
|         [Tooltip("Enable to use where-allocation NonAlloc KcpServer/Client/Connection versions. Highly recommended on all Unity platforms.")] | ||||
|         public bool NonAlloc = true; | ||||
|  | ||||
|         // server & client (where-allocation NonAlloc versions) | ||||
|         KcpServer server; | ||||
|         KcpClient client; | ||||
|  | ||||
|         // debugging | ||||
|         [Header("Debug")] | ||||
|         public bool debugLog; | ||||
|         // show statistics in OnGUI | ||||
|         public bool statisticsGUI; | ||||
|         // log statistics for headless servers that can't show them in GUI | ||||
|         public bool statisticsLog; | ||||
|  | ||||
|         void Awake() | ||||
|         { | ||||
|             // logging | ||||
|             //   Log.Info should use Debug.Log if enabled, or nothing otherwise | ||||
|             //   (don't want to spam the console on headless servers) | ||||
|             if (debugLog) | ||||
|                 Log.Info = Debug.Log; | ||||
|             else | ||||
|                 Log.Info = _ => {}; | ||||
|             Log.Warning = Debug.LogWarning; | ||||
|             Log.Error = Debug.LogError; | ||||
|  | ||||
|             // client | ||||
|             client = NonAlloc | ||||
|                 ? new KcpClientNonAlloc( | ||||
|                       () => OnClientConnected.Invoke(), | ||||
|                       (message) => OnClientDataReceived.Invoke(message, Channels.Reliable), | ||||
|                       () => OnClientDisconnected.Invoke()) | ||||
|                 : new KcpClient( | ||||
|                       () => OnClientConnected.Invoke(), | ||||
|                       (message) => OnClientDataReceived.Invoke(message, Channels.Reliable), | ||||
|                       () => OnClientDisconnected.Invoke()); | ||||
|  | ||||
|             // server | ||||
|             server = NonAlloc | ||||
|                 ? new KcpServerNonAlloc( | ||||
|                       (connectionId) => OnServerConnected.Invoke(connectionId), | ||||
|                       (connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable), | ||||
|                       (connectionId) => OnServerDisconnected.Invoke(connectionId), | ||||
|                       DualMode, | ||||
|                       NoDelay, | ||||
|                       Interval, | ||||
|                       FastResend, | ||||
|                       CongestionWindow, | ||||
|                       SendWindowSize, | ||||
|                       ReceiveWindowSize, | ||||
|                       Timeout) | ||||
|                 : new KcpServer( | ||||
|                       (connectionId) => OnServerConnected.Invoke(connectionId), | ||||
|                       (connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable), | ||||
|                       (connectionId) => OnServerDisconnected.Invoke(connectionId), | ||||
|                       DualMode, | ||||
|                       NoDelay, | ||||
|                       Interval, | ||||
|                       FastResend, | ||||
|                       CongestionWindow, | ||||
|                       SendWindowSize, | ||||
|                       ReceiveWindowSize, | ||||
|                       Timeout); | ||||
|  | ||||
|             if (statisticsLog) | ||||
|                 InvokeRepeating(nameof(OnLogStatistics), 1, 1); | ||||
|  | ||||
|             Debug.Log("KcpTransport initialized!"); | ||||
|         } | ||||
|  | ||||
|         // all except WebGL | ||||
|         public override bool Available() => | ||||
|             Application.platform != RuntimePlatform.WebGLPlayer; | ||||
|  | ||||
|         // client | ||||
|         public override bool ClientConnected() => client.connected; | ||||
|         public override void ClientConnect(string address) | ||||
|         { | ||||
|             client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); | ||||
|         } | ||||
|         public override void ClientSend(ArraySegment<byte> segment, int channelId) | ||||
|         { | ||||
|             // switch to kcp channel. | ||||
|             // unreliable or reliable. | ||||
|             // default to reliable just to be sure. | ||||
|             switch (channelId) | ||||
|             { | ||||
|                 case Channels.Unreliable: | ||||
|                     client.Send(segment, KcpChannel.Unreliable); | ||||
|                     break; | ||||
|                 default: | ||||
|                     client.Send(segment, KcpChannel.Reliable); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         public override void ClientDisconnect() => client.Disconnect(); | ||||
|         // process incoming in early update | ||||
|         public override void ClientEarlyUpdate() | ||||
|         { | ||||
|             // scene change messages disable transports to stop them from | ||||
|             // processing while changing the scene. | ||||
|             // -> we need to check enabled here | ||||
|             // -> and in kcp's internal loops, see Awake() OnCheckEnabled setup! | ||||
|             // (see also: https://github.com/vis2k/Mirror/pull/379) | ||||
|             if (enabled) client.TickIncoming(); | ||||
|         } | ||||
|         // process outgoing in late update | ||||
|         public override void ClientLateUpdate() => client.TickOutgoing(); | ||||
|  | ||||
|         // scene change message will disable transports. | ||||
|         // kcp processes messages in an internal loop which should be | ||||
|         // stopped immediately after scene change (= after disabled) | ||||
|         // => kcp has tests to guaranteed that calling .Pause() during the | ||||
|         //    receive loop stops the receive loop immediately, not after. | ||||
|         void OnEnable() | ||||
|         { | ||||
|             // unpause when enabled again | ||||
|             client?.Unpause(); | ||||
|             server?.Unpause(); | ||||
|         } | ||||
|  | ||||
|         void OnDisable() | ||||
|         { | ||||
|             // pause immediately when not enabled anymore | ||||
|             client?.Pause(); | ||||
|             server?.Pause(); | ||||
|         } | ||||
|  | ||||
|         // server | ||||
|         public override Uri ServerUri() | ||||
|         { | ||||
|             UriBuilder builder = new UriBuilder(); | ||||
|             builder.Scheme = Scheme; | ||||
|             builder.Host = Dns.GetHostName(); | ||||
|             builder.Port = Port; | ||||
|             return builder.Uri; | ||||
|         } | ||||
|         public override bool ServerActive() => server.IsActive(); | ||||
|         public override void ServerStart() => server.Start(Port); | ||||
|         public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId) | ||||
|         { | ||||
|             // switch to kcp channel. | ||||
|             // unreliable or reliable. | ||||
|             // default to reliable just to be sure. | ||||
|             switch (channelId) | ||||
|             { | ||||
|                 case Channels.Unreliable: | ||||
|                     server.Send(connectionId, segment, KcpChannel.Unreliable); | ||||
|                     break; | ||||
|                 default: | ||||
|                     server.Send(connectionId, segment, KcpChannel.Reliable); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         public override void ServerDisconnect(int connectionId) =>  server.Disconnect(connectionId); | ||||
|         public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId); | ||||
|         public override void ServerStop() => server.Stop(); | ||||
|         public override void ServerEarlyUpdate() | ||||
|         { | ||||
|             // scene change messages disable transports to stop them from | ||||
|             // processing while changing the scene. | ||||
|             // -> we need to check enabled here | ||||
|             // -> and in kcp's internal loops, see Awake() OnCheckEnabled setup! | ||||
|             // (see also: https://github.com/vis2k/Mirror/pull/379) | ||||
|             if (enabled) server.TickIncoming(); | ||||
|         } | ||||
|         // process outgoing in late update | ||||
|         public override void ServerLateUpdate() => server.TickOutgoing(); | ||||
|  | ||||
|         // common | ||||
|         public override void Shutdown() {} | ||||
|  | ||||
|         // max message size | ||||
|         public override int GetMaxPacketSize(int channelId = Channels.Reliable) | ||||
|         { | ||||
|             // switch to kcp channel. | ||||
|             // unreliable or reliable. | ||||
|             // default to reliable just to be sure. | ||||
|             switch (channelId) | ||||
|             { | ||||
|                 case Channels.Unreliable: | ||||
|                     return KcpConnection.UnreliableMaxMessageSize; | ||||
|                 default: | ||||
|                     return KcpConnection.ReliableMaxMessageSize; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // kcp reliable channel max packet size is MTU * WND_RCV | ||||
|         // this allows 144kb messages. but due to head of line blocking, all | ||||
|         // other messages would have to wait until the maxed size one is | ||||
|         // delivered. batching 144kb messages each time would be EXTREMELY slow | ||||
|         // and fill the send queue nearly immediately when using it over the | ||||
|         // network. | ||||
|         // => instead we always use MTU sized batches. | ||||
|         // => people can still send maxed size if needed. | ||||
|         public override int GetBatchThreshold(int channelId) => | ||||
|             KcpConnection.UnreliableMaxMessageSize; | ||||
|  | ||||
|         // server statistics | ||||
|         // LONG to avoid int overflows with connections.Sum. | ||||
|         // see also: https://github.com/vis2k/Mirror/pull/2777 | ||||
|         public long GetAverageMaxSendRate() => | ||||
|             server.connections.Count > 0 | ||||
|                 ? server.connections.Values.Sum(conn => (long)conn.MaxSendRate) / server.connections.Count | ||||
|                 : 0; | ||||
|         public long GetAverageMaxReceiveRate() => | ||||
|             server.connections.Count > 0 | ||||
|                 ? server.connections.Values.Sum(conn => (long)conn.MaxReceiveRate) / server.connections.Count | ||||
|                 : 0; | ||||
|         long GetTotalSendQueue() => | ||||
|             server.connections.Values.Sum(conn => conn.SendQueueCount); | ||||
|         long GetTotalReceiveQueue() => | ||||
|             server.connections.Values.Sum(conn => conn.ReceiveQueueCount); | ||||
|         long GetTotalSendBuffer() => | ||||
|             server.connections.Values.Sum(conn => conn.SendBufferCount); | ||||
|         long GetTotalReceiveBuffer() => | ||||
|             server.connections.Values.Sum(conn => conn.ReceiveBufferCount); | ||||
|  | ||||
|         // PrettyBytes function from DOTSNET | ||||
|         // pretty prints bytes as KB/MB/GB/etc. | ||||
|         // long to support > 2GB | ||||
|         // divides by floats to return "2.5MB" etc. | ||||
|         public static string PrettyBytes(long bytes) | ||||
|         { | ||||
|             // bytes | ||||
|             if (bytes < 1024) | ||||
|                 return $"{bytes} B"; | ||||
|             // kilobytes | ||||
|             else if (bytes < 1024L * 1024L) | ||||
|                 return $"{(bytes / 1024f):F2} KB"; | ||||
|             // megabytes | ||||
|             else if (bytes < 1024 * 1024L * 1024L) | ||||
|                 return $"{(bytes / (1024f * 1024f)):F2} MB"; | ||||
|             // gigabytes | ||||
|             return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; | ||||
|         } | ||||
|  | ||||
| // OnGUI allocates even if it does nothing. avoid in release. | ||||
| #if UNITY_EDITOR || DEVELOPMENT_BUILD | ||||
|         void OnGUI() | ||||
|         { | ||||
|             if (!statisticsGUI) return; | ||||
|  | ||||
|             GUILayout.BeginArea(new Rect(5, 110, 300, 300)); | ||||
|  | ||||
|             if (ServerActive()) | ||||
|             { | ||||
|                 GUILayout.BeginVertical("Box"); | ||||
|                 GUILayout.Label("SERVER"); | ||||
|                 GUILayout.Label($"  connections: {server.connections.Count}"); | ||||
|                 GUILayout.Label($"  MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s"); | ||||
|                 GUILayout.Label($"  MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s"); | ||||
|                 GUILayout.Label($"  SendQueue: {GetTotalSendQueue()}"); | ||||
|                 GUILayout.Label($"  ReceiveQueue: {GetTotalReceiveQueue()}"); | ||||
|                 GUILayout.Label($"  SendBuffer: {GetTotalSendBuffer()}"); | ||||
|                 GUILayout.Label($"  ReceiveBuffer: {GetTotalReceiveBuffer()}"); | ||||
|                 GUILayout.EndVertical(); | ||||
|             } | ||||
|  | ||||
|             if (ClientConnected()) | ||||
|             { | ||||
|                 GUILayout.BeginVertical("Box"); | ||||
|                 GUILayout.Label("CLIENT"); | ||||
|                 GUILayout.Label($"  MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s"); | ||||
|                 GUILayout.Label($"  MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s"); | ||||
|                 GUILayout.Label($"  SendQueue: {client.connection.SendQueueCount}"); | ||||
|                 GUILayout.Label($"  ReceiveQueue: {client.connection.ReceiveQueueCount}"); | ||||
|                 GUILayout.Label($"  SendBuffer: {client.connection.SendBufferCount}"); | ||||
|                 GUILayout.Label($"  ReceiveBuffer: {client.connection.ReceiveBufferCount}"); | ||||
|                 GUILayout.EndVertical(); | ||||
|             } | ||||
|  | ||||
|             GUILayout.EndArea(); | ||||
|         } | ||||
| #endif | ||||
|  | ||||
|         void OnLogStatistics() | ||||
|         { | ||||
|             if (ServerActive()) | ||||
|             { | ||||
|                 string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n"; | ||||
|                 log += $"  connections: {server.connections.Count}\n"; | ||||
|                 log += $"  MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n"; | ||||
|                 log += $"  MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n"; | ||||
|                 log += $"  SendQueue: {GetTotalSendQueue()}\n"; | ||||
|                 log += $"  ReceiveQueue: {GetTotalReceiveQueue()}\n"; | ||||
|                 log += $"  SendBuffer: {GetTotalSendBuffer()}\n"; | ||||
|                 log += $"  ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; | ||||
|                 Debug.Log(log); | ||||
|             } | ||||
|  | ||||
|             if (ClientConnected()) | ||||
|             { | ||||
|                 string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; | ||||
|                 log += $"  MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s\n"; | ||||
|                 log += $"  MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s\n"; | ||||
|                 log += $"  SendQueue: {client.connection.SendQueueCount}\n"; | ||||
|                 log += $"  ReceiveQueue: {client.connection.ReceiveQueueCount}\n"; | ||||
|                 log += $"  SendBuffer: {client.connection.SendBufferCount}\n"; | ||||
|                 log += $"  ReceiveBuffer: {client.connection.ReceiveBufferCount}\n\n"; | ||||
|                 Debug.Log(log); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override string ToString() => "KCP"; | ||||
|     } | ||||
| } | ||||
| //#endif MIRROR <- commented out because MIRROR isn't defined on first import yet | ||||
| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 6b0fecffa3f624585964b0d0eb21b18e | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 71a1c8e8c022d4731a481c1808f37e5d | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										24
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2016 limpo1989 | ||||
| Copyright (c) 2020 Paul Pacheco | ||||
| Copyright (c) 2020 Lymdun | ||||
| Copyright (c) 2020 vis2k | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										7
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 9a3e8369060cf4e94ac117603de47aa6 | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										94
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| V1.12 [2021-07-16] | ||||
| - where-allocation removed. will be optional in the future. | ||||
| - Tests: don't depend on Unity anymore | ||||
| - fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls | ||||
|   OnDisconnected to let the user now. | ||||
| - fix: KcpServer.DualMode is now configurable in the constructor instead of | ||||
|   using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too. | ||||
|  | ||||
| V1.11 rollback [2021-06-01] | ||||
| - perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime | ||||
|   resizing/allocations | ||||
|  | ||||
| V1.10 [2021-05-28] | ||||
| - feature: configurable Timeout | ||||
| - allocations explained with comments (C# ReceiveFrom / IPEndPoint.GetHashCode) | ||||
| - fix: #17 KcpConnection.ReceiveNextReliable now assigns message default so it | ||||
|   works in .net too | ||||
| - fix: Segment pool is not static anymore. Each kcp instance now has it's own | ||||
|   Pool<Segment>. fixes #18 concurrency issues | ||||
|  | ||||
| V1.9 [2021-03-02] | ||||
| - Tick() split into TickIncoming()/TickOutgoing() to use in Mirror's new update | ||||
|   functions. allows to minimize latency. | ||||
|   => original Tick() is still supported for convenience. simply processes both! | ||||
|  | ||||
| V1.8 [2021-02-14] | ||||
| - fix: Unity IPv6 errors on Nintendo Switch | ||||
| - fix: KcpConnection now disconnects if data message was received without content. | ||||
|   previously it would call OnData with an empty ArraySegment, causing all kinds of | ||||
|   weird behaviour in Mirror/DOTSNET. Added tests too. | ||||
| - fix: KcpConnection.SendData: don't allow sending empty messages anymore. disconnect | ||||
|   and log a warning to make it completely obvious. | ||||
|  | ||||
| V1.7 [2021-01-13] | ||||
| - fix: unreliable messages reset timeout now too | ||||
| - perf: KcpConnection OnCheckEnabled callback changed to a simple 'paused' boolean. | ||||
|   This is faster than invoking a Func<bool> every time and allows us to fix #8 more | ||||
|   easily later by calling .Pause/.Unpause from OnEnable/OnDisable in MirrorTransport. | ||||
| - fix #8: Unpause now resets timeout to fix a bug where Mirror would pause kcp, | ||||
|   change the scene which took >10s, then unpause and kcp would detect the lack of | ||||
|   any messages for >10s as timeout. Added test to make sure it never happens again. | ||||
| - MirrorTransport: statistics logging for headless servers | ||||
| - Mirror Transport: Send/Receive window size increased once more from 2048 to 4096. | ||||
|  | ||||
| V1.6 [2021-01-10] | ||||
| - Unreliable channel added! | ||||
| - perf: KcpHeader byte added to every kcp message to indicate | ||||
|   Handshake/Data/Ping/Disconnect instead of scanning each message for Hello/Byte/Ping | ||||
|   content via SegmentEquals. It's a lot cleaner, should be faster and should avoid | ||||
|   edge cases where a message content would equal Hello/Ping/Bye sequence accidentally. | ||||
| - Kcp.Input: offset moved to parameters for cases where it's needed | ||||
| - Kcp.SetMtu from original Kcp.c | ||||
|  | ||||
| V1.5 [2021-01-07] | ||||
| - KcpConnection.MaxSend/ReceiveRate calculation based on the article | ||||
| - MirrorTransport: large send/recv window size defaults to avoid high latencies caused | ||||
|   by packets not being processed fast enough | ||||
| - MirrorTransport: show MaxSend/ReceiveRate in debug gui | ||||
| - MirrorTransport: don't Log.Info to console in headless mode if debug log is disabled | ||||
|  | ||||
| V1.4 [2020-11-27] | ||||
| - fix: OnCheckEnabled added. KcpConnection message processing while loop can now | ||||
|   be interrupted immediately. fixes Mirror Transport scene changes which need to stop | ||||
|   processing any messages immediately after a scene message) | ||||
| - perf: Mirror KcpTransport: FastResend enabled by default. turbo mode according to: | ||||
|   https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration | ||||
| - perf: Mirror KcpTransport: CongestionControl disabled by default (turbo mode) | ||||
|  | ||||
| V1.3 [2020-11-17] | ||||
| - Log.Info/Warning/Error so logging doesn't depend on UnityEngine anymore | ||||
| - fix: Server.Tick catches SocketException which happens if Android client is killed | ||||
| - MirrorTransport: debugLog option added that can be checked in Unity Inspector | ||||
| - Utils.Clamp so Kcp.cs doesn't depend on UnityEngine | ||||
| - Utils.SegmentsEqual: use Linq SequenceEqual so it doesn't depend on UnityEngine | ||||
| => kcp2k can now be used in any C# project even without Unity | ||||
|  | ||||
| V1.2 [2020-11-10] | ||||
| - more tests added | ||||
| - fix: raw receive buffers are now all of MTU size | ||||
| - fix: raw receive detects error where buffer was too small for msgLength and | ||||
|   result in excess data being dropped silently | ||||
| - KcpConnection.MaxMessageSize added for use in high level | ||||
| - KcpConnection.MaxMessageSize increased from 1200 bytes to to maximum allowed | ||||
|   message size of 145KB for kcp (based on mtu, overhead, wnd_rcv) | ||||
|  | ||||
| V1.1 [2020-10-30] | ||||
| - high level cleanup, fixes, improvements | ||||
|  | ||||
| V1.0 [2020-10-22] | ||||
| - Kcp.cs now mirrors original Kcp.c behaviour | ||||
|   (this fixes dozens of bugs) | ||||
|  | ||||
| V0.1 | ||||
| - initial kcp-csharp based version | ||||
							
								
								
									
										7
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: ed3f2cf1bbf1b4d53a6f2c103d311f71 | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 5a54d18b954cb4407a28b633fc32ea6d | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -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 | ||||
							
								
								
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 5cafb8851a0084f3e94a580c207b3923 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,3 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| [assembly: InternalsVisibleTo("kcp2k.Tests")] | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: aec6a15ac7bd43129317ea1f01f19782 | ||||
| timeCreated: 1602665988 | ||||
							
								
								
									
										1042
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1042
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: a59b1cae10a334faf807432ab472f212 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										46
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // Pool to avoid allocations (from libuv2k & Mirror) | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public class Pool<T> | ||||
|     { | ||||
|         // Mirror is single threaded, no need for concurrent collections | ||||
|         readonly Stack<T> objects = new Stack<T>(); | ||||
|  | ||||
|         // some types might need additional parameters in their constructor, so | ||||
|         // we use a Func<T> generator | ||||
|         readonly Func<T> objectGenerator; | ||||
|  | ||||
|         // some types might need additional cleanup for returned objects | ||||
|         readonly Action<T> objectResetter; | ||||
|  | ||||
|         public Pool(Func<T> objectGenerator, Action<T> objectResetter, int initialCapacity) | ||||
|         { | ||||
|             this.objectGenerator = objectGenerator; | ||||
|             this.objectResetter = objectResetter; | ||||
|  | ||||
|             // allocate an initial pool so we have fewer (if any) | ||||
|             // allocations in the first few frames (or seconds). | ||||
|             for (int i = 0; i < initialCapacity; ++i) | ||||
|                 objects.Push(objectGenerator()); | ||||
|         } | ||||
|  | ||||
|         // take an element from the pool, or create a new one if empty | ||||
|         public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator(); | ||||
|  | ||||
|         // return an element to the pool | ||||
|         public void Return(T item) | ||||
|         { | ||||
|             objectResetter(item); | ||||
|             objects.Push(item); | ||||
|         } | ||||
|  | ||||
|         // clear the pool | ||||
|         public void Clear() => objects.Clear(); | ||||
|  | ||||
|         // count to see how many objects are in the pool. useful for tests. | ||||
|         public int Count => objects.Count; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 35c07818fc4784bb4ba472c8e5029002 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										62
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using System.IO; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     // KCP Segment Definition | ||||
|     internal class Segment | ||||
|     { | ||||
|         internal uint conv;     // conversation | ||||
|         internal uint cmd;      // command, e.g. Kcp.CMD_ACK etc. | ||||
|         internal uint frg;      // fragment | ||||
|         internal uint wnd;      // window size that the receive can currently receive | ||||
|         internal uint ts;       // timestamp | ||||
|         internal uint sn;       // serial number | ||||
|         internal uint una; | ||||
|         internal uint resendts; // resend timestamp | ||||
|         internal int rto; | ||||
|         internal uint fastack; | ||||
|         internal uint xmit; | ||||
|  | ||||
|         // we need an auto scaling byte[] with a WriteBytes function. | ||||
|         // MemoryStream does that perfectly, no need to reinvent the wheel. | ||||
|         // note: no need to pool it, because Segment is already pooled. | ||||
|         // -> MTU as initial capacity to avoid most runtime resizing/allocations | ||||
|         internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF); | ||||
|  | ||||
|         // ikcp_encode_seg | ||||
|         // encode a segment into buffer | ||||
|         internal int Encode(byte[] ptr, int offset) | ||||
|         { | ||||
|             int offset_ = offset; | ||||
|             offset += Utils.Encode32U(ptr, offset, conv); | ||||
|             offset += Utils.Encode8u(ptr, offset, (byte)cmd); | ||||
|             offset += Utils.Encode8u(ptr, offset, (byte)frg); | ||||
|             offset += Utils.Encode16U(ptr, offset, (ushort)wnd); | ||||
|             offset += Utils.Encode32U(ptr, offset, ts); | ||||
|             offset += Utils.Encode32U(ptr, offset, sn); | ||||
|             offset += Utils.Encode32U(ptr, offset, una); | ||||
|             offset += Utils.Encode32U(ptr, offset, (uint)data.Position); | ||||
|  | ||||
|             return offset - offset_; | ||||
|         } | ||||
|  | ||||
|         // reset to return a fresh segment to the pool | ||||
|         internal void Reset() | ||||
|         { | ||||
|             conv = 0; | ||||
|             cmd = 0; | ||||
|             frg = 0; | ||||
|             wnd = 0; | ||||
|             ts = 0; | ||||
|             sn = 0; | ||||
|             una = 0; | ||||
|             rto = 0; | ||||
|             xmit = 0; | ||||
|             resendts = 0; | ||||
|             fastack = 0; | ||||
|  | ||||
|             // keep buffer for next pool usage, but reset length (= bytes written) | ||||
|             data.SetLength(0); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: fc58706a05dd3442c8fde858d5266855 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										76
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| namespace kcp2k | ||||
| { | ||||
|     public static partial class Utils | ||||
|     { | ||||
|         // Clamp so we don't have to depend on UnityEngine | ||||
|         public static int Clamp(int value, int min, int max) | ||||
|         { | ||||
|             if (value < min) return min; | ||||
|             if (value > max) return max; | ||||
|             return value; | ||||
|         } | ||||
|  | ||||
|         // encode 8 bits unsigned int | ||||
|         public static int Encode8u(byte[] p, int offset, byte c) | ||||
|         { | ||||
|             p[0 + offset] = c; | ||||
|             return 1; | ||||
|         } | ||||
|  | ||||
|         // decode 8 bits unsigned int | ||||
|         public static int Decode8u(byte[] p, int offset, ref byte c) | ||||
|         { | ||||
|             c = p[0 + offset]; | ||||
|             return 1; | ||||
|         } | ||||
|  | ||||
|         // encode 16 bits unsigned int (lsb) | ||||
|         public static int Encode16U(byte[] p, int offset, ushort w) | ||||
|         { | ||||
|             p[0 + offset] = (byte)(w >> 0); | ||||
|             p[1 + offset] = (byte)(w >> 8); | ||||
|             return 2; | ||||
|         } | ||||
|  | ||||
|         // decode 16 bits unsigned int (lsb) | ||||
|         public static int Decode16U(byte[] p, int offset, ref ushort c) | ||||
|         { | ||||
|             ushort result = 0; | ||||
|             result |= p[0 + offset]; | ||||
|             result |= (ushort)(p[1 + offset] << 8); | ||||
|             c = result; | ||||
|             return 2; | ||||
|         } | ||||
|  | ||||
|         // encode 32 bits unsigned int (lsb) | ||||
|         public static int Encode32U(byte[] p, int offset, uint l) | ||||
|         { | ||||
|             p[0 + offset] = (byte)(l >> 0); | ||||
|             p[1 + offset] = (byte)(l >> 8); | ||||
|             p[2 + offset] = (byte)(l >> 16); | ||||
|             p[3 + offset] = (byte)(l >> 24); | ||||
|             return 4; | ||||
|         } | ||||
|  | ||||
|         // decode 32 bits unsigned int (lsb) | ||||
|         public static int Decode32U(byte[] p, int offset, ref uint c) | ||||
|         { | ||||
|             uint result = 0; | ||||
|             result |= p[0 + offset]; | ||||
|             result |= (uint)(p[1 + offset] << 8); | ||||
|             result |= (uint)(p[2 + offset] << 16); | ||||
|             result |= (uint)(p[3 + offset] << 24); | ||||
|             c = result; | ||||
|             return 4; | ||||
|         } | ||||
|  | ||||
|         // timediff was a macro in original Kcp. let's inline it if possible. | ||||
|         [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|         public static int TimeDiff(uint later, uint earlier) | ||||
|         { | ||||
|             return (int)(later - earlier); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: ef959eb716205bd48b050f010a9a35ae | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
							
								
								
									
										15
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| { | ||||
|     "name": "kcp2k", | ||||
|     "references": [ | ||||
|         "GUID:63c380d6dae6946209ed0832388a657c" | ||||
|     ], | ||||
|     "includePlatforms": [], | ||||
|     "excludePlatforms": [], | ||||
|     "allowUnsafeCode": true, | ||||
|     "overrideReferences": false, | ||||
|     "precompiledReferences": [], | ||||
|     "autoReferenced": true, | ||||
|     "defineConstraints": [], | ||||
|     "versionDefines": [], | ||||
|     "noEngineReferences": false | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 6806a62c384838046a3c66c44f06d75f | ||||
| AssemblyDefinitionImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: e9de45e025f26411bbb52d1aefc8d5a5 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2021 Mirror Networking (vis2k, FakeByte) | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in | ||||
| all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
| THE SOFTWARE. | ||||
| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: a857d4e863bbf4a7dba70bc2cd1b5949 | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,8 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 6b7f3f8e8fa16475bbe48a8e9fbe800b | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,3 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| [assembly: InternalsVisibleTo("where-allocations.Tests")] | ||||
| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 158a96a7489b450485a8b06a13328871 | ||||
| timeCreated: 1622356221 | ||||
| @@ -0,0 +1,58 @@ | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace WhereAllocation | ||||
| { | ||||
|     public static class Extensions | ||||
|     { | ||||
|         // always pass the same IPEndPointNonAlloc instead of allocating a new | ||||
|         // one each time. | ||||
|         // | ||||
|         // use IPEndPointNonAlloc.temp to get the latest SocketAdddress written | ||||
|         // by ReceiveFrom_Internal! | ||||
|         // | ||||
|         // IMPORTANT: .temp will be overwritten in next call! | ||||
|         //            hash or manually copy it if you need to store it, e.g. | ||||
|         //            when adding a new connection. | ||||
|         public static int ReceiveFrom_NonAlloc( | ||||
|             this Socket socket, | ||||
|             byte[] buffer, | ||||
|             int offset, | ||||
|             int size, | ||||
|             SocketFlags socketFlags, | ||||
|             IPEndPointNonAlloc remoteEndPoint) | ||||
|         { | ||||
|             // call ReceiveFrom with IPEndPointNonAlloc. | ||||
|             // need to wrap this in ReceiveFrom_NonAlloc because it's not | ||||
|             // obvious that IPEndPointNonAlloc.Create does NOT create a new | ||||
|             // IPEndPoint. it saves the result in IPEndPointNonAlloc.temp! | ||||
|             EndPoint casted = remoteEndPoint; | ||||
|             return  socket.ReceiveFrom(buffer, offset, size, socketFlags, ref casted); | ||||
|         } | ||||
|  | ||||
|         // same as above, different parameters | ||||
|         public static int ReceiveFrom_NonAlloc(this Socket socket, byte[] buffer, IPEndPointNonAlloc remoteEndPoint) | ||||
|         { | ||||
|             EndPoint casted = remoteEndPoint; | ||||
|             return socket.ReceiveFrom(buffer, ref casted); | ||||
|         } | ||||
|  | ||||
|         // SendTo allocates too: | ||||
|         // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L2240 | ||||
|         // -> the allocation is in EndPoint.Serialize() | ||||
|         // NOTE: technically this function isn't necessary. | ||||
|         //       could just pass IPEndPointNonAlloc. | ||||
|         //       still good for strong typing. | ||||
|         public static int SendTo_NonAlloc( | ||||
|             this Socket socket, | ||||
|             byte[] buffer, | ||||
|             int offset, | ||||
|             int size, | ||||
|             SocketFlags socketFlags, | ||||
|             IPEndPointNonAlloc remoteEndPoint) | ||||
|         { | ||||
|             EndPoint casted = remoteEndPoint; | ||||
|             return socket.SendTo(buffer, offset, size, socketFlags, casted); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 9e801942544d44d65808fb250623fe25 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,208 @@ | ||||
| using System; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
|  | ||||
| namespace WhereAllocation | ||||
| { | ||||
|     public class IPEndPointNonAlloc : IPEndPoint | ||||
|     { | ||||
|         // Two steps to remove allocations in ReceiveFrom_Internal: | ||||
|         // | ||||
|         // 1.) remoteEndPoint.Serialize(): | ||||
|         //     https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1733 | ||||
|         //     -> creates an EndPoint for ReceiveFrom_Internal to write into | ||||
|         //     -> it's never read from: | ||||
|         //        ReceiveFrom_Internal passes it to native: | ||||
|         //          https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1885 | ||||
|         //        native recv populates 'sockaddr* from' with the remote address: | ||||
|         //          https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom | ||||
|         //     -> can NOT be null. bricks both Unity and Unity Hub otherwise. | ||||
|         //     -> it seems as if Serialize() is only called to avoid allocating | ||||
|         //        a 'new SocketAddress' in ReceiveFrom. it's up to the EndPoint. | ||||
|         // | ||||
|         // 2.) EndPoint.Create(SocketAddress): | ||||
|         //     https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 | ||||
|         //     -> SocketAddress is the remote's address that we want to return | ||||
|         //     -> to avoid 'new EndPoint(SocketAddress), it seems up to the user | ||||
|         //        to decide how to create a new EndPoint via .Create | ||||
|         //     -> SocketAddress is the object that was returned by Serialize() | ||||
|         // | ||||
|         // in other words, all we need is an extra SocketAddress field that we | ||||
|         // can pass to ReceiveFrom_Internal to write the result into. | ||||
|         // => callers can then get the result from the extra field! | ||||
|         // => no allocations | ||||
|         // | ||||
|         // IMPORTANT: remember that IPEndPointNonAlloc is always the same object | ||||
|         //            and never changes. only the helper field is changed. | ||||
|         public SocketAddress temp; | ||||
|  | ||||
|         // constructors simply create the field once by calling the base method. | ||||
|         // (our overwritten method would create anything new) | ||||
|         public IPEndPointNonAlloc(long address, int port) : base(address, port) | ||||
|         { | ||||
|             temp = base.Serialize(); | ||||
|         } | ||||
|         public IPEndPointNonAlloc(IPAddress address, int port) : base(address, port) | ||||
|         { | ||||
|             temp = base.Serialize(); | ||||
|         } | ||||
|  | ||||
|         // Serialize simply returns it | ||||
|         public override SocketAddress Serialize() => temp; | ||||
|  | ||||
|         // Create doesn't need to create anything. | ||||
|         // SocketAddress object is already the one we returned in Serialize(). | ||||
|         // ReceiveFrom_Internal simply wrote into it. | ||||
|         public override EndPoint Create(SocketAddress socketAddress) | ||||
|         { | ||||
|             // original IPEndPoint.Create validates: | ||||
|             if (socketAddress.Family != AddressFamily) | ||||
|                 throw new ArgumentException($"Unsupported socketAddress.AddressFamily: {socketAddress.Family}. Expected: {AddressFamily}"); | ||||
|             if (socketAddress.Size < 8) | ||||
|                 throw new ArgumentException($"Unsupported socketAddress.Size: {socketAddress.Size}. Expected: <8"); | ||||
|  | ||||
|             // double check to guarantee that ReceiveFrom actually did write | ||||
|             // into our 'temp' field. just in case that's ever changed. | ||||
|             if (socketAddress != temp) | ||||
|             { | ||||
|                 // well this is fun. | ||||
|                 // in the latest mono from the above github links, | ||||
|                 // the result of Serialize() is passed as 'ref' so ReceiveFrom | ||||
|                 // does in fact write into it. | ||||
|                 // | ||||
|                 // in Unity 2019 LTS's mono version, it does create a new one | ||||
|                 // each time. this is from ILSpy Receive_From: | ||||
|                 // | ||||
|                 //     SocketPal.CheckDualModeReceiveSupport(this); | ||||
|                 //     ValidateBlockingMode(); | ||||
|                 //     if (NetEventSource.IsEnabled) | ||||
|                 //     { | ||||
|                 //         NetEventSource.Info(this, $"SRC{LocalEndPoint} size:{size} remoteEP:{remoteEP}", "ReceiveFrom"); | ||||
|                 //     } | ||||
|                 //     EndPoint remoteEP2 = remoteEP; | ||||
|                 //     System.Net.Internals.SocketAddress socketAddress = SnapshotAndSerialize(ref remoteEP2); | ||||
|                 //     System.Net.Internals.SocketAddress socketAddress2 = IPEndPointExtensions.Serialize(remoteEP2); | ||||
|                 //     int bytesTransferred; | ||||
|                 //     SocketError socketError = SocketPal.ReceiveFrom(_handle, buffer, offset, size, socketFlags, socketAddress.Buffer, ref socketAddress.InternalSize, out bytesTransferred); | ||||
|                 //     SocketException ex = null; | ||||
|                 //     if (socketError != 0) | ||||
|                 //     { | ||||
|                 //         ex = new SocketException((int)socketError); | ||||
|                 //         UpdateStatusAfterSocketError(ex); | ||||
|                 //         if (NetEventSource.IsEnabled) | ||||
|                 //         { | ||||
|                 //             NetEventSource.Error(this, ex, "ReceiveFrom"); | ||||
|                 //         } | ||||
|                 //         if (ex.SocketErrorCode != SocketError.MessageSize) | ||||
|                 //         { | ||||
|                 //             throw ex; | ||||
|                 //         } | ||||
|                 //     } | ||||
|                 //     if (!socketAddress2.Equals(socketAddress)) | ||||
|                 //     { | ||||
|                 //         try | ||||
|                 //         { | ||||
|                 //             remoteEP = remoteEP2.Create(socketAddress); | ||||
|                 //         } | ||||
|                 //         catch | ||||
|                 //         { | ||||
|                 //         } | ||||
|                 //         if (_rightEndPoint == null) | ||||
|                 //         { | ||||
|                 //             _rightEndPoint = remoteEP2; | ||||
|                 //         } | ||||
|                 //     } | ||||
|                 //     if (ex != null) | ||||
|                 //     { | ||||
|                 //         throw ex; | ||||
|                 //     } | ||||
|                 //     if (NetEventSource.IsEnabled) | ||||
|                 //     { | ||||
|                 //         NetEventSource.DumpBuffer(this, buffer, offset, size, "ReceiveFrom"); | ||||
|                 //         NetEventSource.Exit(this, bytesTransferred, "ReceiveFrom"); | ||||
|                 //     } | ||||
|                 //     return bytesTransferred; | ||||
|                 // | ||||
|  | ||||
|                 // so until they upgrade their mono version, we are stuck with | ||||
|                 // some allocations. | ||||
|                 // | ||||
|                 // for now, let's pass the newly created on to our temp so at | ||||
|                 // least we reuse it next time. | ||||
|                 temp = socketAddress; | ||||
|  | ||||
|                 // SocketAddress.GetHashCode() depends on SocketAddress.m_changed. | ||||
|                 // ReceiveFrom only sets the buffer, it does not seem to set m_changed. | ||||
|                 // we need to reset m_changed for two reasons: | ||||
|                 // * if m_changed is false, GetHashCode() returns the cahced m_hash | ||||
|                 //   which is '0'. that would be a problem. | ||||
|                 //   https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L262 | ||||
|                 // * if we have a cached m_hash, but ReceiveFrom modified the buffer | ||||
|                 //   then the GetHashCode() should change too. so we need to reset | ||||
|                 //   either way. | ||||
|                 // | ||||
|                 // the only way to do that is by _actually_ modifying the buffer: | ||||
|                 // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L99 | ||||
|                 // so let's do that. | ||||
|                 // -> unchecked in case it's byte.Max | ||||
|                 unchecked | ||||
|                 { | ||||
|                     temp[0] += 1; | ||||
|                     temp[0] -= 1; | ||||
|                 } | ||||
|  | ||||
|                 // make sure this worked. | ||||
|                 // at least throw an Exception to make it obvious if the trick does | ||||
|                 // not work anymore, in case ReceiveFrom is ever changed. | ||||
|                 if (temp.GetHashCode() == 0) | ||||
|                     throw new Exception($"SocketAddress GetHashCode() is 0 after ReceiveFrom. Does the m_changed trick not work anymore?"); | ||||
|  | ||||
|                 // in the future, enable this again: | ||||
|                 //throw new Exception($"Socket.ReceiveFrom(): passed SocketAddress={socketAddress} but expected {temp}. This should never happen. Did ReceiveFrom() change?"); | ||||
|             } | ||||
|  | ||||
|             // ReceiveFrom sets seed_endpoint to the result of Create(): | ||||
|             // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1764 | ||||
|             // so let's return ourselves at least. | ||||
|             // (seed_endpoint only seems to matter for BeginSend etc.) | ||||
|             return this; | ||||
|         } | ||||
|  | ||||
|         // we need to overwrite GetHashCode() for two reasons. | ||||
|         // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPEndPoint.cs#L160 | ||||
|         // * it uses m_Address. but our true SocketAddress is in m_temp. | ||||
|         //   m_Address might not be set at all. | ||||
|         // * m_Address.GetHashCode() allocates: | ||||
|         //   https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 | ||||
|         public override int GetHashCode() => temp.GetHashCode(); | ||||
|  | ||||
|         // helper function to create an ACTUAL new IPEndPoint from this. | ||||
|         // server needs it to store new connections as unique IPEndPoints. | ||||
|         public IPEndPoint DeepCopyIPEndPoint() | ||||
|         { | ||||
|             // we need to create a new IPEndPoint from 'temp' SocketAddress. | ||||
|             // there is no 'new IPEndPoint(SocketAddress) constructor. | ||||
|             // so we need to be a bit creative... | ||||
|  | ||||
|             // allocate a placeholder IPAddress to copy | ||||
|             // our SocketAddress into. | ||||
|             // -> needs to be the same address family. | ||||
|             IPAddress ipAddress; | ||||
|             if (temp.Family == AddressFamily.InterNetworkV6) | ||||
|                 ipAddress = IPAddress.IPv6Any; | ||||
|             else if (temp.Family == AddressFamily.InterNetwork) | ||||
|                 ipAddress = IPAddress.Any; | ||||
|             else | ||||
|                 throw new Exception($"Unexpected SocketAddress family: {temp.Family}"); | ||||
|  | ||||
|             // allocate a placeholder IPEndPoint | ||||
|             // with the needed size form IPAddress. | ||||
|             // (the real class. not NonAlloc) | ||||
|             IPEndPoint placeholder = new IPEndPoint(ipAddress, 0); | ||||
|  | ||||
|             // the real IPEndPoint's .Create function can create a new IPEndPoint | ||||
|             // copy from a SocketAddress. | ||||
|             return (IPEndPoint)placeholder.Create(temp); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: af0279d15e39b484792394f1d3cad4d9 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|     "name": "where-allocations", | ||||
|     "references": [], | ||||
|     "includePlatforms": [], | ||||
|     "excludePlatforms": [], | ||||
|     "allowUnsafeCode": false, | ||||
|     "overrideReferences": false, | ||||
|     "precompiledReferences": [], | ||||
|     "autoReferenced": true, | ||||
|     "defineConstraints": [], | ||||
|     "versionDefines": [], | ||||
|     "noEngineReferences": false | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 63c380d6dae6946209ed0832388a657c | ||||
| AssemblyDefinitionImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -0,0 +1,2 @@ | ||||
| V0.1 [2021-06-01] | ||||
| - initial release | ||||
| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: f1256cadc037546ccb66071784fce137 | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
		Reference in New Issue
	
	Block a user
	 DerTyp187
					DerTyp187