perf(net): own one pooled async receive
Replace timeout-polled in-world UDP receives with one cancellable caller-buffered socket operation. Transfer only right-sized pooled datagrams through the FIFO, return every ownership edge deterministically, and send caller spans without a transport copy while preserving handshake pacing and ACK order.
This commit is contained in:
parent
a2a1e5916d
commit
7211bb1bf7
10 changed files with 547 additions and 87 deletions
|
|
@ -4,22 +4,20 @@ using System.Net.Sockets;
|
|||
namespace AcDream.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum-viable UDP transport for acdream. Wraps a <see cref="UdpClient"/>
|
||||
/// with synchronous send + timeout-based receive — good enough for the
|
||||
/// Phase 4.6 handshake smoke test and early state-machine bring-up.
|
||||
/// UDP transport for acdream. The live world owns one cancellable,
|
||||
/// caller-buffered asynchronous receive after the synchronous handshake.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Not yet provided</b> (deferred to a later phase once the handshake
|
||||
/// actually works): background receive thread, outbound queue, ack/retransmit
|
||||
/// window, heartbeat timer, concurrent send/receive. The acdream game loop
|
||||
/// will need a real async pump eventually but building that now would be
|
||||
/// debugging two things at once when we hit the first protocol mismatch.
|
||||
/// Sends consume caller spans directly through <see cref="Socket"/> without
|
||||
/// materializing a transport-only array copy.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class NetClient : IDisposable
|
||||
{
|
||||
private readonly UdpClient _udp;
|
||||
private readonly IPEndPoint _remote;
|
||||
private readonly IPEndPoint _anyRemote =
|
||||
new(IPAddress.Any, 0);
|
||||
|
||||
/// <summary>Last <see cref="Socket.ReceiveTimeout"/> value applied to the
|
||||
/// underlying socket, so repeated <see cref="Receive"/> calls with the
|
||||
|
|
@ -57,7 +55,10 @@ public sealed class NetClient : IDisposable
|
|||
/// </summary>
|
||||
public void Send(ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
_udp.Send(datagram.ToArray(), datagram.Length, _remote);
|
||||
_udp.Client.SendTo(
|
||||
datagram,
|
||||
SocketFlags.None,
|
||||
_remote);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -68,7 +69,63 @@ public sealed class NetClient : IDisposable
|
|||
/// </summary>
|
||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
_udp.Send(datagram.ToArray(), datagram.Length, remote);
|
||||
_udp.Client.SendTo(
|
||||
datagram,
|
||||
SocketFlags.None,
|
||||
remote);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive into caller-owned storage. Returns the received byte count,
|
||||
/// or <c>-1</c> when <paramref name="timeout"/> expires.
|
||||
/// </summary>
|
||||
public int Receive(
|
||||
Span<byte> destination,
|
||||
TimeSpan timeout,
|
||||
out IPEndPoint? from)
|
||||
{
|
||||
int timeoutMs = checked((int)timeout.TotalMilliseconds);
|
||||
if (timeoutMs != _lastAppliedReceiveTimeoutMs)
|
||||
{
|
||||
_udp.Client.ReceiveTimeout = timeoutMs;
|
||||
_lastAppliedReceiveTimeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EndPoint remote = _anyRemote;
|
||||
int length = _udp.Client.ReceiveFrom(
|
||||
destination,
|
||||
SocketFlags.None,
|
||||
ref remote);
|
||||
from = (IPEndPoint)remote;
|
||||
return length;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
when (ex.SocketErrorCode == SocketError.TimedOut)
|
||||
{
|
||||
from = null;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One cancellable asynchronous receive into caller-owned storage.
|
||||
/// The returned memory remains owned by the caller.
|
||||
/// </summary>
|
||||
public async ValueTask<NetReceiveResult> ReceiveAsync(
|
||||
Memory<byte> destination,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SocketReceiveFromResult result =
|
||||
await _udp.Client.ReceiveFromAsync(
|
||||
destination,
|
||||
SocketFlags.None,
|
||||
_anyRemote,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return new NetReceiveResult(
|
||||
result.ReceivedBytes,
|
||||
(IPEndPoint)result.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -79,7 +136,7 @@ public sealed class NetClient : IDisposable
|
|||
/// </summary>
|
||||
public byte[]? Receive(TimeSpan timeout, out IPEndPoint? from)
|
||||
{
|
||||
int timeoutMs = (int)timeout.TotalMilliseconds;
|
||||
int timeoutMs = checked((int)timeout.TotalMilliseconds);
|
||||
if (timeoutMs != _lastAppliedReceiveTimeoutMs)
|
||||
{
|
||||
_udp.Client.ReceiveTimeout = timeoutMs;
|
||||
|
|
@ -88,11 +145,12 @@ public sealed class NetClient : IDisposable
|
|||
try
|
||||
{
|
||||
IPEndPoint any = new(IPAddress.Any, 0);
|
||||
var bytes = _udp.Receive(ref any);
|
||||
byte[] bytes = _udp.Receive(ref any);
|
||||
from = any;
|
||||
return bytes;
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
|
||||
catch (SocketException ex)
|
||||
when (ex.SocketErrorCode == SocketError.TimedOut)
|
||||
{
|
||||
from = null;
|
||||
return null;
|
||||
|
|
@ -129,3 +187,10 @@ public sealed class NetClient : IDisposable
|
|||
|
||||
public void Dispose() => _udp.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of receiving one UDP datagram into caller-owned memory.
|
||||
/// </summary>
|
||||
public readonly record struct NetReceiveResult(
|
||||
int Length,
|
||||
IPEndPoint RemoteEndPoint);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue