mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
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:
parent
69de3f0379
commit
8d857de09b
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user