fix: kcp2k V1.12 (updated)

-> where-allocation now optional to handle platforms that don't support it
This commit is contained in:
vis2k 2021-07-16 20:21:43 +08:00
parent d66d228079
commit 6d021c0875
15 changed files with 236 additions and 50 deletions

View File

@ -34,8 +34,10 @@ public class KcpTransport : Transport
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. 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.")] [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. 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 // server & client (where-allocation NonAlloc versions)
KcpServer server; KcpServer server;
KcpClient client; KcpClient client;
@ -60,26 +62,42 @@ void Awake()
Log.Error = Debug.LogError; Log.Error = Debug.LogError;
// client // client
client = new KcpClient( client = NonAlloc
() => OnClientConnected.Invoke(), ? new KcpClientNonAlloc(
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable), () => OnClientConnected.Invoke(),
() => OnClientDisconnected.Invoke() (message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
); () => OnClientDisconnected.Invoke())
: new KcpClient(
() => OnClientConnected.Invoke(),
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
() => OnClientDisconnected.Invoke());
// server // server
server = new KcpServer( server = NonAlloc
(connectionId) => OnServerConnected.Invoke(connectionId), ? new KcpServerNonAlloc(
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable), (connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId) => OnServerDisconnected.Invoke(connectionId), (connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
DualMode, (connectionId) => OnServerDisconnected.Invoke(connectionId),
NoDelay, DualMode,
Interval, NoDelay,
FastResend, Interval,
CongestionWindow, FastResend,
SendWindowSize, CongestionWindow,
ReceiveWindowSize, SendWindowSize,
Timeout 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) if (statisticsLog)
InvokeRepeating(nameof(OnLogStatistics), 1, 1); InvokeRepeating(nameof(OnLogStatistics), 1, 1);

View File

@ -22,6 +22,11 @@ public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action O
this.OnDisconnected = OnDisconnected; 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) 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) if (connected)
@ -30,7 +35,8 @@ public void Connect(string address, ushort port, bool noDelay, uint interval, in
return; return;
} }
connection = new KcpClientConnection(); // create connection
connection = CreateConnection();
// setup events // setup events
connection.OnAuthenticated = () => connection.OnAuthenticated = () =>

View File

@ -28,6 +28,15 @@ public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
} }
} }
// 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) 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}"); Log.Info($"KcpClient: connect to {host}:{port}");
@ -35,9 +44,14 @@ public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.
// try resolve host name // try resolve host name
if (ResolveHostname(host, out IPAddress[] addresses)) if (ResolveHostname(host, out IPAddress[] addresses))
{ {
remoteEndpoint = new IPEndPoint(addresses[0], port); // create remote endpoint
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); CreateRemoteEndPoint(addresses, port);
socket.Connect(remoteEndpoint);
// 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); SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
// client should send handshake to server as very first message // client should send handshake to server as very first message
@ -49,6 +63,7 @@ public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.
else OnDisconnected(); else OnDisconnected();
} }
// call from transport update // call from transport update
public void RawReceive() public void RawReceive()
{ {
@ -58,7 +73,7 @@ public void RawReceive()
{ {
while (socket.Poll(0, SelectMode.SelectRead)) while (socket.Poll(0, SelectMode.SelectRead))
{ {
int msgLength = socket.ReceiveFrom(rawReceiveBuffer, ref remoteEndpoint); int msgLength = ReceiveFrom(rawReceiveBuffer);
// IMPORTANT: detect if buffer was too small for the // IMPORTANT: detect if buffer was too small for the
// received msgLength. otherwise the excess // received msgLength. otherwise the excess
// data would be silently lost. // data would be silently lost.

View File

@ -10,7 +10,7 @@ enum KcpState { Connected, Authenticated, Disconnected }
public abstract class KcpConnection public abstract class KcpConnection
{ {
protected Socket socket; protected Socket socket;
protected EndPoint remoteEndpoint; protected EndPoint remoteEndPoint;
internal Kcp kcp; internal Kcp kcp;
// kcp can have several different states, let's use a state machine // kcp can have several different states, let's use a state machine
@ -650,7 +650,7 @@ public void Disconnect()
} }
// get remote endpoint // get remote endpoint
public EndPoint GetRemoteEndPoint() => remoteEndpoint; public EndPoint GetRemoteEndPoint() => remoteEndPoint;
// pause/unpause to safely support mirror scene handling and to // pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed. // immediately pause the receive while loop if needed.

View File

@ -43,7 +43,7 @@ public class KcpServer
public int Timeout; public int Timeout;
// state // state
Socket socket; protected Socket socket;
EndPoint newClientEP; EndPoint newClientEP;
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
@ -137,6 +137,33 @@ public string GetClientAddress(int connectionId)
return ""; 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. // process incoming messages. should be called before updating the world.
HashSet<int> connectionsToRemove = new HashSet<int>(); HashSet<int> connectionsToRemove = new HashSet<int>();
public void TickIncoming() public void TickIncoming()
@ -145,25 +172,10 @@ public void TickIncoming()
{ {
try try
{ {
// NOTE: ReceiveFrom allocates. // receive
// we pass our IPEndPoint to ReceiveFrom. int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId);
// 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 msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP);
//Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// calculate connectionId 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.
int connectionId = newClientEP.GetHashCode();
// IMPORTANT: detect if buffer was too small for the received // IMPORTANT: detect if buffer was too small for the received
// msgLength. otherwise the excess data would be // msgLength. otherwise the excess data would be
// silently lost. // silently lost.
@ -173,8 +185,9 @@ public void TickIncoming()
// is this a new connection? // is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{ {
// create a new KcpConnection // create a new KcpConnection based on last received
connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); // EndPoint. can be overwritten for where-allocation.
connection = CreateConnection();
// DO NOT add to connections yet. only if the first message // DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either: // is actually the kcp handshake. otherwise it's either:
@ -205,7 +218,7 @@ public void TickIncoming()
// add to connections dict after being authenticated. // add to connections dict after being authenticated.
connections.Add(connectionId, connection); connections.Add(connectionId, connection);
Log.Info($"KCP: server added connection({connectionId}): {newClientEP}"); Log.Info($"KCP: server added connection({connectionId})");
// setup Data + Disconnected events only AFTER the // setup Data + Disconnected events only AFTER the
// handshake. we don't want to fire OnServerDisconnected // handshake. we don't want to fire OnServerDisconnected

View File

@ -5,16 +5,18 @@ namespace kcp2k
{ {
public class KcpServerConnection : KcpConnection public class KcpServerConnection : KcpConnection
{ {
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) // 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.socket = socket;
this.remoteEndpoint = remoteEndpoint; this.remoteEndPoint = remoteEndPoint;
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
} }
protected override void RawSend(byte[] data, int length) protected override void RawSend(byte[] data, int length)
{ {
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndpoint); socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint);
} }
} }
} }

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b320ff06046474eae7bce7240ea478c
timeCreated: 1626430641

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4c1b235bbe054706bef6d092f361006e
timeCreated: 1626430539

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2cf0ccf7d551480bb5af08fcbe169f84
timeCreated: 1626435264

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e1b74cc224b4c83a0f6c8d8da9090ab
timeCreated: 1626430608

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54b8398dcd544c8a93bcad846214cc40
timeCreated: 1626432191