-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and Motivation
QUIC is finally a proposed standard in RFC, with HTTP/3 and WebTransport on the way.
To prepare for WebTransport and other use cases, such as unreliable message delivery in SignalR, .NET should implement QUIC datagram API, as MsQuic already supports it, to enable higher-level APIs such as WebTransport.
Until WebTransport is standardized, it may be used today to stream real-time game state and ultra low-latency voice data where dropped packets should not be retransmitted. Once this is done, SignalR may also support new features.
Proposed API
namespace System.Net.Quic
{
public class QuicConnectionOptions
{
+ public bool DatagramReceiveEnabled { get { throw null; } set { } }
}
+ public delegate void QuicDatagramReceivedEventHandler(QuicConnection sender, ReadOnlySpan<byte> buffer);
+ public enum QuicDatagramSendState
+ {
+ Unknown,
+ Sent,
+ LostSuspect,
+ Lost,
+ Acknowledged,
+ AcknowledgedSuprious,
+ Canceled
+ }
+ public delegate void QuicDatagramSendStateChangedHandler(QuicDatagramSendState state, bool isFinal);
+ public sealed class QuicDatagramSendOptions
+ {
+ public bool Priority { get { throw null; } set { } }
+ public QuicDatagramSendStateChangedHandler? StateChanged { get { throw null; } set { } }
+ }
public class QuicConnection
{
+ public bool DatagramReceivedEnabled { get { throw null; } set { } }
+ public bool DatagramSendEnabled { get { throw null; } set { } }
+ public int DatagramMaxSendLength { get { throw null; } }
+ public event QuicDatagramReceivedEventHandler? DatagramReceived { add { } remove { } }
+ public void SendDatagram(ReadOnlyMemory<byte> buffer, QuicDatagramSendOptions? options = null) { throw null; }
+ public void SendDatagram(System.Buffers.ReadOnlySequence<byte> buffers, QuicDatagramSendOptions? options = null) { throw null; }
}
}See https://github.com/wegylexy/quic-with-datagram for implementation (with MsQuic 1.9.0).
Usage Examples
// receive
connection.DatagramReceived += (sender, buffer) =>
{
// Parse the readonly span synchronously, without copying all the bytes, into an async task
MyAsyncChannel.Writer.TryWrite(MyZeroCopyHandler.HandleAsync(buffer));
}
// send
var size = Unsafe.SizeOf<MyTinyStruct>();
Debug.Assert(size <= connection.DatagramMaxSendLength);
TaskCompletionSource tcs = new();
// note: max send length may vary throughout the connection
var array = ArrayPool<byte>.Shared.Rent(size);
try
{
MemoryMarshal.Cast<byte, MyTinyStruct>(array).SetCurrentGameState();
// may prefix with a ReadOnlyMemory<byte> of a WebTransport session ID into a ReadOnlySequence<byte>
connection.SendDatagram(new ReadOnlyMemory<byte>(array, 0, size), new()
{
StateChanged = (state, isFinal) =>
{
if (isFinal)
{
tcs.TrySetResult();
}
Console.WriteLine(state);
};
});
await tcs.Task; // wait until it is safe to return the array back to the pool
}
catch when (size > connection.DatagramMaxSendLength)
{
Console.Error.WriteLine("Datagram max send length reduced, sending canceled.")
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}Alternative Designs
Receiving datagram buffers with a channel (similar to the stream API) was considered, but the MsQuic datagram buffer is merely a pointer to the underlying UDP buffer such that the buffer content is only available during the event callback. Async handler implies unnecessary cloning of possibly a thousand bytes and increase GC pressure for every single datagram received.
Sending with a readonly span was considered for stackalloced buffer, but MsQuic needs to hold on to the memory until the datagram send state becomes final.