123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- //#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;
- using Unity.Collections;
- using UnityEngine.Serialization;
- namespace kcp2k
- {
- [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
- [DisallowMultipleComponent]
- public class KcpTransport : Transport, PortTransport
- {
- // scheme used by this transport
- public const string Scheme = "kcp";
- // common
- [Header("Transport Configuration")]
- [FormerlySerializedAs("Port")]
- public ushort port = 7777;
- public ushort Port { get => port; set => port=value; }
- [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;
- [Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
- public int RecvBufferSize = 1024 * 1027 * 7;
- [Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
- public int SendBufferSize = 1024 * 1027 * 7;
- [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. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")]
- /*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. This also increases max message size.")]
- public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 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 SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
- [Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")]
- public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x.
- [Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")]
- [FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")]
- public bool MaximizeSocketBuffers = true;
- [Header("Allowed Max Message Sizes\nBased on Receive Window Size")]
- [Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")]
- [ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate
- [Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")]
- [ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate
- // config is created from the serialized properties above.
- // we can expose the config directly in the future.
- // for now, let's not break people's old settings.
- protected KcpConfig config;
- // use default MTU for this transport.
- const int MTU = Kcp.MTU_DEF;
- // server & client
- protected KcpServer server;
- protected 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;
- // translate Kcp <-> Mirror channels
- public static int FromKcpChannel(KcpChannel channel) =>
- channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable;
- public static KcpChannel ToKcpChannel(int channel) =>
- channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable;
- public static TransportError ToTransportError(ErrorCode error)
- {
- switch(error)
- {
- case ErrorCode.DnsResolve: return TransportError.DnsResolve;
- case ErrorCode.Timeout: return TransportError.Timeout;
- case ErrorCode.Congestion: return TransportError.Congestion;
- case ErrorCode.InvalidReceive: return TransportError.InvalidReceive;
- case ErrorCode.InvalidSend: return TransportError.InvalidSend;
- case ErrorCode.ConnectionClosed: return TransportError.ConnectionClosed;
- case ErrorCode.Unexpected: return TransportError.Unexpected;
- default: throw new InvalidCastException($"KCP: missing error translation for {error}");
- }
- }
- protected virtual 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;
- // create config from serialized settings
- config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
- // client (NonAlloc version is not necessary anymore)
- client = new KcpClient(
- () => OnClientConnected.Invoke(),
- (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
- () => OnClientDisconnected.Invoke(),
- (error, reason) => OnClientError.Invoke(ToTransportError(error), reason),
- config
- );
- // server
- server = new KcpServer(
- (connectionId) => OnServerConnected.Invoke(connectionId),
- (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
- (connectionId) => OnServerDisconnected.Invoke(connectionId),
- (connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason),
- config
- );
- if (statisticsLog)
- InvokeRepeating(nameof(OnLogStatistics), 1, 1);
- Log.Info("KcpTransport initialized!");
- }
- protected virtual void OnValidate()
- {
- // show max message sizes in inspector for convenience.
- // 'config' isn't available in edit mode yet, so use MTU define.
- ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize);
- UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU);
- }
- // all except WebGL
- // Do not change this back to using Application.platform
- // because that doesn't work in the Editor!
- public override bool Available() =>
- #if UNITY_WEBGL
- false;
- #else
- true;
- #endif
- // client
- public override bool ClientConnected() => client.connected;
- public override void ClientConnect(string address)
- {
- client.Connect(address, Port);
- }
- public override void ClientConnect(Uri uri)
- {
- if (uri.Scheme != Scheme)
- throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
- int serverPort = uri.IsDefaultPort ? Port : uri.Port;
- client.Connect(uri.Host, (ushort)serverPort);
- }
- public override void ClientSend(ArraySegment<byte> segment, int channelId)
- {
- client.Send(segment, ToKcpChannel(channelId));
- // call event. might be null if no statistics are listening etc.
- OnClientDataSent?.Invoke(segment, channelId);
- }
- public override void ClientDisconnect() => client.Disconnect();
- // process incoming in early update
- public override void ClientEarlyUpdate()
- {
- // only process messages while transport is enabled.
- // scene change messsages disable it to stop processing.
- // (see also: https://github.com/vis2k/Mirror/pull/379)
- if (enabled) client.TickIncoming();
- }
- // process outgoing in late update
- public override void ClientLateUpdate() => client.TickOutgoing();
- // 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)
- {
- server.Send(connectionId, segment, ToKcpChannel(channelId));
- // call event. might be null if no statistics are listening etc.
- OnServerDataSent?.Invoke(connectionId, segment, channelId);
- }
- public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
- public override string ServerGetClientAddress(int connectionId)
- {
- IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
- return endPoint != null
- // Map to IPv4 if "IsIPv4MappedToIPv6"
- // "::ffff:127.0.0.1" -> "127.0.0.1"
- ? (endPoint.Address.IsIPv4MappedToIPv6
- ? endPoint.Address.MapToIPv4().ToString()
- : endPoint.Address.ToString())
- : "";
- }
- public override void ServerStop() => server.Stop();
- public override void ServerEarlyUpdate()
- {
- // only process messages while transport is enabled.
- // scene change messsages disable it to stop processing.
- // (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 KcpPeer.UnreliableMaxMessageSize(config.Mtu);
- default:
- return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize);
- }
- }
- // 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) =>
- KcpPeer.UnreliableMaxMessageSize(config.Mtu);
- // 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 => conn.MaxSendRate) / server.connections.Count
- : 0;
- public long GetAverageMaxReceiveRate() =>
- server.connections.Count > 0
- ? server.connections.Values.Sum(conn => 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";
- }
- protected virtual void OnGUIStatistics()
- {
- 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.MaxSendRate)}/s");
- GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s");
- GUILayout.Label($" SendQueue: {client.SendQueueCount}");
- GUILayout.Label($" ReceiveQueue: {client.ReceiveQueueCount}");
- GUILayout.Label($" SendBuffer: {client.SendBufferCount}");
- GUILayout.Label($" ReceiveBuffer: {client.ReceiveBufferCount}");
- GUILayout.EndVertical();
- }
- GUILayout.EndArea();
- }
- // OnGUI allocates even if it does nothing. avoid in release.
- #if UNITY_EDITOR || DEVELOPMENT_BUILD
- protected virtual void OnGUI()
- {
- if (statisticsGUI) OnGUIStatistics();
- }
- #endif
- protected virtual 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";
- Log.Info(log);
- }
- if (ClientConnected())
- {
- string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
- log += $" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s\n";
- log += $" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s\n";
- log += $" SendQueue: {client.SendQueueCount}\n";
- log += $" ReceiveQueue: {client.ReceiveQueueCount}\n";
- log += $" SendBuffer: {client.SendBufferCount}\n";
- log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n";
- Log.Info(log);
- }
- }
- public override string ToString() => $"KCP {port}";
- }
- }
- //#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|