fix: kcp2k V1.18

- feature: OnError to allow higher level to show popups etc.
- feature: KcpServer.GetClientAddress is now GetClientEndPoint in order to
  expose more details
- ResolveHostname: include exception in log for easier debugging
- fix: KcpClientConnection.RawReceive now logs the SocketException even if
  it was expected. makes debugging easier.
- fix: KcpServer.TickIncoming now logs the SocketException even if it was
  expected. makes debugging easier.
- fix: KcpClientConnection.RawReceive now calls Disconnect() if the other end
  has closed the connection. better than just remaining in a state with unusable
  sockets.

=> error handling based on #3155
=> fixes #3143
This commit is contained in:
vis2k 2022-05-08 10:36:07 +08:00
parent c65eed8ea8
commit 35f1f225f3
10 changed files with 126 additions and 42 deletions

View File

@ -90,11 +90,13 @@ void Awake()
? new KcpClientNonAlloc(
() => OnClientConnected.Invoke(),
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
() => OnClientDisconnected.Invoke())
() => OnClientDisconnected.Invoke(),
(error) => OnClientError.Invoke(new Exception(error)))
: new KcpClient(
() => OnClientConnected.Invoke(),
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
() => OnClientDisconnected.Invoke());
() => OnClientDisconnected.Invoke(),
(error) => OnClientError.Invoke(new Exception(error)));
// server
server = NonAlloc
@ -102,6 +104,7 @@ void Awake()
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
(connectionId, error) => OnServerError.Invoke(connectionId, new Exception(error)),
DualMode,
NoDelay,
Interval,
@ -116,6 +119,7 @@ void Awake()
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
(connectionId, error) => OnServerError.Invoke(connectionId, new Exception(error)),
DualMode,
NoDelay,
Interval,
@ -196,7 +200,11 @@ public override void ServerSend(int connectionId, ArraySegment<byte> segment, in
OnServerDataSent?.Invoke(connectionId, segment, channelId);
}
public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
public override string ServerGetClientAddress(int connectionId)
{
IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
return endPoint != null ? endPoint.Address.ToString() : "";
}
public override void ServerStop() => server.Stop();
public override void ServerEarlyUpdate()
{

View File

@ -1,3 +1,16 @@
V1.18 [2022-05-08]
- feature: OnError to allow higher level to show popups etc.
- feature: KcpServer.GetClientAddress is now GetClientEndPoint in order to
expose more details
- ResolveHostname: include exception in log for easier debugging
- fix: KcpClientConnection.RawReceive now logs the SocketException even if
it was expected. makes debugging easier.
- fix: KcpServer.TickIncoming now logs the SocketException even if it was
expected. makes debugging easier.
- fix: KcpClientConnection.RawReceive now calls Disconnect() if the other end
has closed the connection. better than just remaining in a state with unusable
sockets.
V1.17 [2022-01-09]
- perf: server/client MaximizeSendReceiveBuffersToOSLimit option to set send/recv
buffer sizes to OS limit. avoids drops due to small buffers under heavy load.

View File

@ -10,16 +10,21 @@ public class KcpClient
public Action OnConnected;
public Action<ArraySegment<byte>, KcpChannel> OnData;
public Action OnDisconnected;
// error callback instead of logging.
// allows libraries to show popups etc.
// (string instead of Exception for ease of use)
public Action<string> OnError;
// state
public KcpClientConnection connection;
public bool connected;
public KcpClient(Action OnConnected, Action<ArraySegment<byte>, KcpChannel> OnData, Action OnDisconnected)
public KcpClient(Action OnConnected, Action<ArraySegment<byte>, KcpChannel> OnData, Action OnDisconnected, Action<string> OnError)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.OnError = OnError;
}
// CreateConnection can be overwritten for where-allocation:
@ -53,19 +58,23 @@ public void Connect(string address,
{
Log.Info($"KCP: OnClientConnected");
connected = true;
OnConnected.Invoke();
OnConnected();
};
connection.OnData = (message, channel) =>
{
//Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(message, channel);
OnData(message, channel);
};
connection.OnDisconnected = () =>
{
Log.Info($"KCP: OnClientDisconnected");
connected = false;
connection = null;
OnDisconnected.Invoke();
OnDisconnected();
};
connection.OnError = (exception) =>
{
OnError(exception);
};
// connect

View File

@ -21,9 +21,9 @@ public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
addresses = Dns.GetHostAddresses(hostname);
return addresses.Length >= 1;
}
catch (SocketException)
catch (SocketException exception)
{
Log.Info($"Failed to resolve host: {hostname}");
Log.Info($"Failed to resolve host: {hostname} reason: {exception}");
addresses = null;
return false;
}
@ -95,7 +95,12 @@ public void Connect(string host,
RawReceive();
}
// otherwise call OnDisconnected to let the user know.
else OnDisconnected();
else
{
// pass error to user callback. no need to log it manually.
OnError($"Failed to resolve host: {host}");
OnDisconnected();
}
}
// call from transport update
@ -119,14 +124,22 @@ public void RawReceive()
}
else
{
Log.Error($"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
// pass error to user callback. no need to log it manually.
OnError($"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) {}
catch (SocketException ex)
{
// the other end closing the connection is not an 'error'.
// but connections should never just end silently.
// at least log a message for easier debugging.
Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}");
Disconnect();
}
}
protected override void Dispose()

View File

@ -19,6 +19,10 @@ public abstract class KcpConnection
public Action OnAuthenticated;
public Action<ArraySegment<byte>, KcpChannel> OnData;
public Action OnDisconnected;
// error callback instead of logging.
// allows libraries to show popups etc.
// (string instead of Exception for ease of use)
public Action<string> OnError;
// If we don't receive anything these many milliseconds
// then consider us disconnected
@ -166,7 +170,8 @@ void HandleTimeout(uint time)
// 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.");
// pass error to user callback. no need to log it manually.
OnError($"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting.");
Disconnect();
}
}
@ -176,7 +181,8 @@ void HandleDeadLink()
// kcp has 'dead_link' detection. might as well use it.
if (kcp.state == -1)
{
Log.Warning($"KCP Connection dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting.");
// pass error to user callback. no need to log it manually.
OnError($"KCP Connection dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting.");
Disconnect();
}
}
@ -203,10 +209,11 @@ void HandleChoked()
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");
// pass error to user callback. no need to log it manually.
OnError($"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.");
// let's clear all pending sends before disconnting with 'Bye'.
// otherwise a single Flush in Disconnect() won't be enough to
@ -242,7 +249,8 @@ bool ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message)
else
{
// if receive failed, close everything
Log.Warning($"Receive failed with error={received}. closing connection.");
// pass error to user callback. no need to log it manually.
OnError($"Receive failed with error={received}. closing connection.");
Disconnect();
}
}
@ -250,7 +258,8 @@ bool ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message)
// 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.");
// pass error to user callback. no need to log it manually.
OnError($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
Disconnect();
}
}
@ -292,7 +301,8 @@ void TickIncoming_Connected(uint time)
case KcpHeader.Disconnect:
{
// everything else is not allowed during handshake!
Log.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
// pass error to user callback. no need to log it manually.
OnError($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
Disconnect();
break;
}
@ -332,7 +342,8 @@ void TickIncoming_Authenticated(uint time)
// empty data = attacker, or something went wrong
else
{
Log.Warning("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
// pass error to user callback. no need to log it manually.
OnError("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
Disconnect();
}
break;
@ -381,19 +392,22 @@ public void TickIncoming()
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
// pass error to user callback. no need to log it manually.
OnError($"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.");
// pass error to user callback. no need to log it manually.
OnError($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
catch (Exception exception)
{
// unexpected
Log.Error(ex.ToString());
// pass error to user callback. no need to log it manually.
OnError($"KCP Connection: unexpected Exception: {exception}");
Disconnect();
}
}
@ -423,19 +437,22 @@ public void TickOutgoing()
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
// pass error to user callback. no need to log it manually.
OnError($"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.");
// pass error to user callback. no need to log it manually.
OnError($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
catch (Exception exception)
{
// unexpected
Log.Error(ex.ToString());
// pass error to user callback. no need to log it manually.
OnError($"KCP Connection: unexpected exception: {exception}");
Disconnect();
}
}
@ -496,8 +513,9 @@ public void RawInput(byte[] buffer, int msgLength)
}
else
{
// should never
Log.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
// should never happen
// pass error to user callback. no need to log it manually.
OnError($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
Disconnect();
}
break;
@ -505,7 +523,8 @@ public void RawInput(byte[] buffer, int msgLength)
default:
{
// not a valid channel. random data or attacks.
Log.Info($"Disconnecting connection because of invalid channel header: {channel}");
// pass error to user callback. no need to log it manually.
OnError($"Disconnecting connection because of invalid channel header: {channel}");
Disconnect();
break;
}
@ -580,7 +599,8 @@ public void SendData(ArraySegment<byte> data, KcpChannel channel)
// 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.");
// pass error to user callback. no need to log it manually.
OnError("KcpConnection: tried sending empty message. This should never happen. Disconnecting.");
Disconnect();
return;
}

View File

@ -13,6 +13,10 @@ public class KcpServer
public Action<int> OnConnected;
public Action<int, ArraySegment<byte>, KcpChannel> OnData;
public Action<int> OnDisconnected;
// error callback instead of logging.
// allows libraries to show popups etc.
// (string instead of Exception for ease of use)
public Action<int, string> OnError;
// socket configuration
// DualMode uses both IPv6 and IPv4. not all platforms support it.
@ -66,6 +70,7 @@ public class KcpServer
public KcpServer(Action<int> OnConnected,
Action<int, ArraySegment<byte>, KcpChannel> OnData,
Action<int> OnDisconnected,
Action<int, string> OnError,
bool DualMode,
bool NoDelay,
uint Interval,
@ -80,6 +85,7 @@ public KcpServer(Action<int> OnConnected,
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.OnError = OnError;
this.DualMode = DualMode;
this.NoDelay = NoDelay;
this.Interval = Interval;
@ -161,13 +167,14 @@ public void Disconnect(int connectionId)
}
}
public string GetClientAddress(int connectionId)
// expose the whole IPEndPoint, not just the IP address. some need it.
public IPEndPoint GetClientEndPoint(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
return (connection.GetRemoteEndPoint() as IPEndPoint);
}
return "";
return null;
}
// EndPoint & Receive functions can be overwritten for where-allocation:
@ -276,12 +283,18 @@ public void TickIncoming()
// call mirror event
Log.Info($"KCP: OnServerDisconnected({connectionId})");
OnDisconnected.Invoke(connectionId);
OnDisconnected(connectionId);
};
// setup error event
connection.OnError = (error) =>
{
OnError(connectionId, error);
};
// finally, call mirror OnConnected event
Log.Info($"KCP: OnServerConnected({connectionId})");
OnConnected.Invoke(connectionId);
OnConnected(connectionId);
};
// now input the message & process received ones
@ -308,7 +321,13 @@ public void TickIncoming()
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
catch (SocketException ex)
{
// the other end closing the connection is not an 'error'.
// but connections should never just end silently.
// at least log a message for easier debugging.
Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}");
}
}
// process inputs for all server connections

View File

@ -6,8 +6,8 @@ namespace kcp2k
{
public class KcpClientNonAlloc : KcpClient
{
public KcpClientNonAlloc(Action OnConnected, Action<ArraySegment<byte>, KcpChannel> OnData, Action OnDisconnected)
: base(OnConnected, OnData, OnDisconnected)
public KcpClientNonAlloc(Action OnConnected, Action<ArraySegment<byte>, KcpChannel> OnData, Action OnDisconnected, Action<string> OnError)
: base(OnConnected, OnData, OnDisconnected, OnError)
{
}

View File

@ -14,6 +14,7 @@ public class KcpServerNonAlloc : KcpServer
public KcpServerNonAlloc(Action<int> OnConnected,
Action<int, ArraySegment<byte>, KcpChannel> OnData,
Action<int> OnDisconnected,
Action<int, string> OnError,
bool DualMode,
bool NoDelay,
uint Interval,
@ -27,6 +28,7 @@ public KcpServerNonAlloc(Action<int> OnConnected,
: base(OnConnected,
OnData,
OnDisconnected,
OnError,
DualMode,
NoDelay,
Interval,