kcp2k V1.34 [2023-03-15]

- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions.
  to encapsulate WouldBlock allocations, exceptions, etc.
  allows for reuse when overwriting KcpServer/Client (i.e. for relays).
This commit is contained in:
vis2k 2023-03-15 13:00:37 +08:00
parent 69de3f0379
commit 8d857de09b
4 changed files with 194 additions and 107 deletions

View File

@ -1,3 +1,8 @@
V1.34 [2023-03-15]
- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions.
to encapsulate WouldBlock allocations, exceptions, etc.
allows for reuse when overwriting KcpServer/Client (i.e. for relays).
V1.33 [2023-03-14]
- perf: KcpServer/Client RawReceive now call socket.Poll to avoid non-blocking
socket's allocating a new SocketException in case they WouldBlock.

View File

@ -1,6 +1,162 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public static class Extensions
{
// non-blocking UDP send.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool SendToNonBlocking(this Socket socket, ArraySegment<byte> data, EndPoint remoteEP)
{
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
// send to the the endpoint.
// do not send to 'newClientEP', as that's always reused.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP send.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool SendNonBlocking(this Socket socket, ArraySegment<byte> data)
{
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
// SendTo allocates. we used bound Send.
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP receive.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data, ref EndPoint remoteEP)
{
data = default;
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// 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
//
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP);
data = new ArraySegment<byte>(recvBuffer, 0, size);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP receive.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data)
{
data = default;
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// ReceiveFrom allocates. we used bound Receive.
// returns amount of bytes written into buffer.
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
//
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None);
data = new ArraySegment<byte>(recvBuffer, 0, size);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
}
}

View File

@ -131,40 +131,19 @@ protected virtual bool RawReceive(out ArraySegment<byte> segment)
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// ReceiveFrom allocates. we used bound Receive.
// returns amount of bytes written into buffer.
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int msgLength = socket.Receive(rawReceiveBuffer);
//Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
segment = new ArraySegment<byte>(rawReceiveBuffer, 0, msgLength);
return true;
return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
}
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
catch (SocketException e)
{
if (e.SocketErrorCode != SocketError.WouldBlock)
{
// 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.
// for example, his can happen when connecting without a server.
// see test: ConnectWithoutServer().
Log.Info($"KcpClient: looks like the other end has closed the connection. This is fine: {e}");
peer.Disconnect();
}
// WouldBlock indicates there's no data yet, so return false.
// 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.
// for example, his can happen when connecting without a server.
// see test: ConnectWithoutServer().
Log.Info($"KcpClient: looks like the other end has closed the connection. This is fine: {e}");
peer.Disconnect();
return false;
}
}
@ -175,25 +154,11 @@ protected virtual void RawSend(ArraySegment<byte> data)
{
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return;
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
socket.SendNonBlocking(data);
}
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
catch (SocketException e)
{
if (e.SocketErrorCode != SocketError.WouldBlock)
{
Log.Error($"KcpClient: Send failed: {e}");
}
Log.Error($"KcpClient: Send failed: {e}");
}
}

View File

@ -154,53 +154,31 @@ protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int co
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// 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
//
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int size = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP);
segment = new ArraySegment<byte>(rawReceiveBuffer, 0, size);
// set connectionId to hash 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.
connectionId = newClientEP.GetHashCode();
return true;
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
{
// set connectionId to hash 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.
connectionId = newClientEP.GetHashCode();
return true;
}
}
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
catch (SocketException e)
{
if (e.SocketErrorCode != SocketError.WouldBlock)
{
// NOTE: SocketException is not a subclass of IOException.
// 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($"KcpServer: ReceiveFrom failed: {e}");
}
// WouldBlock indicates there's no data yet, so return false.
return false;
// NOTE: SocketException is not a subclass of IOException.
// 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($"KcpServer: ReceiveFrom failed: {e}");
}
return false;
}
// io - out.
@ -217,28 +195,11 @@ protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return;
// send to the the endpoint.
// do not send to 'newClientEP', as that's always reused.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, connection.remoteEndPoint);
socket.SendToNonBlocking(data, connection.remoteEndPoint);
}
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
catch (SocketException e)
{
if (e.SocketErrorCode != SocketError.WouldBlock)
{
Log.Error($"KcpServer: SendTo failed: {e}");
}
Log.Error($"KcpServer: SendTo failed: {e}");
}
}