Background and motivation
API design for exposing QuicConnection and related classes to the public.
The API shape is based on the current internal shape of the class with the exception of merging the ConnectAsync into QuicProvider.CreateConnectionAsync.
Related issues:
Discussed Considerations
- StreamCount/OpenStream parametrize by stream type instead of sets of 2 methods
--> YES - Create with endpoint parameters or put them into options?
a) inside options in ConnectAsync fits better with Socket/SslStream
--> YES, also keep them non-nullable and throw if not connected yet
b) inside ctor allows us to have RemoteEndPoint non-nullable and better aligns with incoming connections (they have both side address, but are not "configured" with options yet)
--> NO - Endpoints are IP endpoints, need to consider on which level we'll do DNS since MsQuic has only crude resolution
- to able to do what
Socket does, means that we need to create MsQuic connection object, try to connect it, let it fail, release it and then repeat for another IP - the resolution cannot be done in
Create, it would need to be done in ConnectAsync
--> Properties are IPEndPoint, provided is EndPoint that can also be DnsEndPoint
- Options in
ConnectAsync - we don't need them past the connection establishement moment so putting them into Create doesn't make much sense
- we might consider collapsing
Create + ConnectAsync (==> we might consider making QuicListenerCreate async as well) - we might keep
Create parameter less, put everything to options and let ConnectAsync deal with it, this is not consistent with QuicListener though (does it matter or not?)
--> YES, options in ConnectAsync, Create is ctor replacement
- Multiple connections scenario: we'll need something like
TryOpenStreamAsync + WaitForStreamAsync (not prototyped yet)
API Proposal
namespaceSystem.Net.Quic;publicsealedclassQuicConnection:IAsyncDisposable{/// <summary>Returns true if QUIC is supported and can be used, e.g. msquic is present, high enough version of TLS is available etc.</summary>publicstaticboolIsSupported{get;}/// <summary>Creates new, fully connected connection configured with the provided options.</summary>/// <exception cref="PlatformNotSupportedException">When <see cref="IsSupported" /> is <c>false</c>.</exception>publicstaticValueTask<QuicConnection>ConnectAsync(QuicConnectionOptionsoptions,CancellationTokencancellationToken=default);/// <summary>Remote endpoint to which the connection is connected.</summary>publicIPEndPointRemoteEndPoint{get;}/// <summary>Local endpoint to which the connection is bound.</summary>publicIPEndPointLocalEndPoint{get;}/// <summary>Peer's certificate, available only if the peer provided the certificate.</summary>publicX509Certificate2?RemoteCertificate{get;}/// <summary>Final, negotiated ALPN.</summary>publicSslApplicationProtocolNegotiatedApplicationProtocol{get;}/// <summary>/// Create an outbound uni/bidirectional stream./// </summary>publicValueTask<QuicStream>OpenOutboundStreamAsync(QuicStreamTypetype,CancellationTokencancellationToken=default);/// <summary>/// Accept an inbound stream./// </summary>publicValueTask<QuicStream>AcceptInboundStreamAsync(CancellationTokencancellationToken=default);/// <summary>/// Close the connection and terminate any active streams./// </summary>publicValueTaskCloseAsync(longerrorCode,CancellationTokencancellationToken=default);/// <summary>/// Silently closes the connection if not closed with CloseAsync beforehand./// </summary>publicvoidDisposeAsync();}/// <summary>Options for a new connection, the same options are used for incoming and outgoing connections.</summary>publicabstractclassQuicConnectionOptions{/// <summary>Prevent user sub-classing.</summary>internalQuicConnectionOptions(){}/// <summary>Limit on the number of bidirectional streams the remote peer connection can create on an open connection.</summary>publicintMaxInboundBidirectionalStreams{get;set;}/// <summary>Limit on the number of unidirectional streams the remote peer connection can create on an open connection.</summary>publicintMaxInboundUnidirectionalStreams{get;set;}/// <summary>Idle timeout for connections, after which the connection will be closed. Zero means using default of the underlying implementation.</summary>publicTimeSpanIdleTimeout{get;set;}=TimeSpan.Zero;/// <summary>Error code used when the stream needs to abort read or write side of the stream internally.</summary>publicrequiredlongDefaultStreamErrorCode{get;set;}}/// <summary>Options for a new connection, only used for outbound connections.</summary>publicsealedclassQuicClientConnectionOptions:QuicConnectionOptions{/// <summary>SSL options for the outgoing connection.</summary>publicrequiredSslClientAuthenticationOptionsClientAuthenticationOptions{get;set;}/// <summary>The endpoint to connect to.</summary>publicrequiredEndPointRemoteEndPoint{get;set;}/// <summary>Optional local endpoint from which the connection is to be established.</summary>publicIPEndPoint?LocalEndPoint{get;set;}publicQuicClientConnectionOptions(){MaxInboundBidirectionalStreams=0;MaxInboundUnidirectionalStreams=0;}}/// <summary>Options for a new connection, only used for incoming connections.</summary>publicsealedclassQuicServerConnectionOptions:QuicConnectionOptions{/// <summary>SSL options for the incoming connection</summary>publicrequiredSslServerAuthenticationOptionsServerAuthenticationOptions{get;set;}publicQuicServerConnectionOptions(){MaxInboundBidirectionalStreams=100;MaxInboundUnidirectionalStreams=10;}}API Usage
Client usage:
varoptions=newQuicClientConnectionOptions(){RemoteEndPoint=newDnsEndPoint("localhost",5001),DefaultStreamErrorCode=(long)Http3ErrorCode.RequestCancelled,ClientAuthenticationOptions=newSslClientAuthenticationOptions(){ApplicationProtocols=newList<SslApplicationProtocol>(){SslApplicationProtocol.Http3},}};awaitusingvarconnection=awaitQuicProvider.CreateConnectionAsync(options,cancellationToken);awaitusingvarstream=awaitconnection.OpenStreamAsync(StreamDirection.Bidirectional,cancellationToken);// Work with stream, open more of them, send and receive data, close them ... https://github.com/dotnet/runtime/issues/69675// Close will terminate all unclosed streams.// If not called, the peer side of the connection will have to wait for idle connection timeout.awaitconnection.CloseAsync((long)Http3ErrorCode.NoError,cancellationToken);// DisposeAsync called by await using.Server usage:
// Consider listener from https://github.com/dotnet/runtime/issues/67560:awaitusingvarconnection=awaitlistener.AcceptConnectionAsync(cancellationToken);while(running){// In case the client closes the connection, Accept will throw appropriate exception.awaitusingvarstream=awaitconnection.AcceptStreamAsync(cancellationToken);// Send and receive data... https://github.com/dotnet/runtime/issues/69675// DisposeAsync called by await using.}// Close will terminate all unclosed streams. Note that H/3 uses GO_AWAY to negotiate graceful connection shutdown with the client.awaitconnection.CloseAsync((long)Http3ErrorCode.NoError,cancellationToken);// DisposeAsync called by await using.Alternative Designs
Risks
As I'll state with all QUIC APIs. We might consider making all of these PreviewFeature. Not to deter user from using it, but to give us flexibility to tune the API shape based on customer feedback.
We don't have many users now and we're mostly making these APIs based on what Kestrel needs, our limited experience with System.Net.Quic and my meager experiments with other QUIC implementations.
Background and motivation
API design for exposing QuicConnection and related classes to the public.
The API shape is based on the current internal shape of the class with the exception of merging the
ConnectAsyncintoQuicProvider.CreateConnectionAsync.Related issues:
QUIC API: Consider passing QuicConnectionOptions to Quic.Connection.ConnectAsync #56009
Consider passing QuicConnectionOptions to Quic.Connection.ConnectAsync: we don't need to hold them just for the meantime between ctor and
ConnectAsync[QUIC] Rewrite Open(Uni|Bidi)rectionalStream to leverage async stream opening #67302
Async stream open: perf need, coming from our benchmarks
HTTP/3: Create additional connections when maximum active streams is reached #51775
Multiple connections: same as H/2 requirement
[API Proposal]: QuicConnection.CancelPendingAcceptStream #60818
CancelPendingAcceptStream: used only for closing the connection, it's not as hot path as originally thought, I'd prefer not to do it now.
[API Proposal]: Expose TLS details for QUIC connection #70184
TLS details for QUIC connection
Discussed Considerations
--> YES
a) inside options in
ConnectAsyncfits better withSocket/SslStream--> YES, also keep them non-nullable and throw if not connected yet
b) inside ctor allows us to have
RemoteEndPointnon-nullable and better aligns with incoming connections (they have both side address, but are not "configured" with options yet)--> NO
Socketdoes, means that we need to create MsQuic connection object, try to connect it, let it fail, release it and then repeat for another IPCreate, it would need to be done inConnectAsync--> Properties are
IPEndPoint, provided isEndPointthat can also beDnsEndPointConnectAsync- we don't need them past the connection establishement moment so putting them intoCreatedoesn't make much senseCreate+ConnectAsync(==> we might consider makingQuicListenerCreateasync as well)Createparameter less, put everything to options and letConnectAsyncdeal with it, this is not consistent withQuicListenerthough (does it matter or not?)--> YES, options in
ConnectAsync,Createis ctor replacementTryOpenStreamAsync+WaitForStreamAsync(not prototyped yet)API Proposal
API Usage
Client usage:
Server usage:
Alternative Designs
QuicConnection.Risks
As I'll state with all QUIC APIs. We might consider making all of these
PreviewFeature. Not to deter user from using it, but to give us flexibility to tune the API shape based on customer feedback.We don't have many users now and we're mostly making these APIs based on what Kestrel needs, our limited experience with System.Net.Quic and my meager experiments with other QUIC implementations.