fix: kcp2k V1.12

- 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.
This commit is contained in:
vis2k 2021-07-16 13:04:21 +08:00
parent 69116bfeb2
commit d66d228079
5 changed files with 72 additions and 70 deletions

View File

@ -16,6 +16,8 @@ public class KcpTransport : Transport
// 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.")]
@ -69,6 +71,7 @@ void Awake()
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
DualMode,
NoDelay,
Interval,
FastResend,

View File

@ -1,6 +1,12 @@
V1.11 [2021-06-01]
- perf: where-allocation (https://github.com/vis2k/where-allocation):
nearly removes all socket.SendTo/ReceiveFrom allocations
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

View File

@ -1,6 +1,5 @@
using System.Net;
using System.Net.Sockets;
using WhereAllocation;
namespace kcp2k
{
@ -13,30 +12,41 @@ public class KcpClientConnection : KcpConnection
// => we need the MTU to fit channel + message!
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
// where-allocation
IPEndPointNonAlloc reusableEP;
// 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;
}
}
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}");
IPAddress[] ipAddress = Dns.GetHostAddresses(host);
if (ipAddress.Length < 1)
throw new SocketException((int)SocketError.HostNotFound);
remoteEndpoint = new IPEndPoint(ipAddress[0], port);
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
// try resolve host name
if (ResolveHostname(host, out IPAddress[] addresses))
{
remoteEndpoint = new IPEndPoint(addresses[0], port);
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
socket.Connect(remoteEndpoint);
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
// create reusableEP with same address family as remoteEndPoint.
// otherwise ReceiveFrom_NonAlloc couldn't use it.
reusableEP = new IPEndPointNonAlloc(ipAddress[0], port);
// client should send handshake to server as very first message
SendHandshake();
socket.Connect(remoteEndpoint);
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
// client should send handshake to server as very first message
SendHandshake();
RawReceive();
RawReceive();
}
// otherwise call OnDisconnected to let the user know.
else OnDisconnected();
}
// call from transport update
@ -48,8 +58,7 @@ public void RawReceive()
{
while (socket.Poll(0, SelectMode.SelectRead))
{
// where-allocation: receive nonalloc.
int msgLength = socket.ReceiveFrom_NonAlloc(rawReceiveBuffer, reusableEP);
int msgLength = socket.ReceiveFrom(rawReceiveBuffer, ref remoteEndpoint);
// IMPORTANT: detect if buffer was too small for the
// received msgLength. otherwise the excess
// data would be silently lost.

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using WhereAllocation;
namespace kcp2k
{
@ -16,6 +15,9 @@ public class KcpServer
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;
@ -42,14 +44,8 @@ public class KcpServer
// state
Socket socket;
#if UNITY_SWITCH
// switch does not support ipv6
//EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
IPEndPointNonAlloc reusableClientEP = new IPEndPointNonAlloc(IPAddress.Any, 0); // where-allocation
#else
//EndPoint newClientEP = new IPEndPoint(IPAddress.IPv6Any, 0);
IPEndPointNonAlloc reusableClientEP = new IPEndPointNonAlloc(IPAddress.IPv6Any, 0); // where-allocation
#endif
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
@ -63,6 +59,7 @@ public class KcpServer
public KcpServer(Action<int> OnConnected,
Action<int, ArraySegment<byte>> OnData,
Action<int> OnDisconnected,
bool DualMode,
bool NoDelay,
uint Interval,
int FastResend = 0,
@ -74,6 +71,7 @@ public KcpServer(Action<int> OnConnected,
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.DualMode = DualMode;
this.NoDelay = NoDelay;
this.Interval = Interval;
this.FastResend = FastResend;
@ -81,6 +79,11 @@ public KcpServer(Action<int> OnConnected,
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;
@ -94,15 +97,19 @@ public void Start(ushort port)
}
// listen
#if UNITY_SWITCH
// Switch does not support ipv6
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(IPAddress.Any, port));
#else
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
socket.DualMode = true;
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
#endif
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)
@ -143,13 +150,9 @@ public void TickIncoming()
// 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);
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)}");
// where-allocation nonalloc ReceiveFrom.
int msgLength = socket.ReceiveFrom_NonAlloc(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, reusableClientEP);
SocketAddress remoteAddress = reusableClientEP.temp;
// calculate connectionId from endpoint
// NOTE: IPEndPoint.GetHashCode() allocates.
// it calls m_Address.GetHashCode().
@ -159,10 +162,7 @@ public void TickIncoming()
//
// => using only newClientEP.Port wouldn't work, because
// different connections can have the same port.
//int connectionId = newClientEP.GetHashCode();
// where-allocation nonalloc GetHashCode
int connectionId = remoteAddress.GetHashCode();
int connectionId = newClientEP.GetHashCode();
// IMPORTANT: detect if buffer was too small for the received
// msgLength. otherwise the excess data would be
@ -173,19 +173,8 @@ public void TickIncoming()
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// 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
// -> where-allocation IPEndPointNonAlloc is reused.
// need to create a new one from the temp address.
connection = new KcpServerConnection(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
// DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either:

View File

@ -1,25 +1,20 @@
using System.Net;
using System.Net.Sockets;
using WhereAllocation;
namespace kcp2k
{
public class KcpServerConnection : KcpConnection
{
IPEndPointNonAlloc reusableSendEndPoint;
public KcpServerConnection(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)
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;
this.reusableSendEndPoint = reusableSendEndPoint;
this.remoteEndpoint = remoteEndpoint;
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
}
protected override void RawSend(byte[] data, int length)
{
// where-allocation nonalloc
socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint);
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndpoint);
}
}
}