Skip to content

QUIC Datagram API #53533

@wegylexy

Description

@wegylexy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions