Skip to content

Commit e377f16

Browse files
wfurtUbuntu
andauthored
basic support for TCP fast open (#99490)
* initial drop * update * cleanup * err * 'test' * windows * sync * feedback & updates * macos * windows * feedback * await * docs * note * macos --------- Co-authored-by: Ubuntu <toweinfu@toweinfu-ubu22.c5goow0wwwee5hembzptmbtr0h.xx.internal.cloudapp.net>
1 parent 7284f17 commit e377f16

File tree

16 files changed

+419
-27
lines changed

16 files changed

+419
-27
lines changed

src/libraries/Common/src/Interop/Unix/System.Native/Interop.Connect.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ internal static partial class Sys
1010
{
1111
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Connect")]
1212
internal static unsafe partial Error Connect(SafeHandle socket, byte* socketAddress, int socketAddressLen);
13+
14+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Connectx")]
15+
internal static unsafe partial Error Connectx(SafeHandle socket, byte* socketAddress, int socketAddressLen, Span<byte> buffer, int bufferLen, int enableTfo, int* sent);
1316
}
1417
}

src/libraries/System.Net.Sockets/ref/System.Net.Sockets.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ public enum SocketOptionName
568568
DropMembership = 13,
569569
DontFragment = 14,
570570
AddSourceMembership = 15,
571+
FastOpen = 15,
571572
DontRoute = 16,
572573
DropSourceMembership = 16,
573574
TcpKeepAliveRetryCount = 16,

src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)</TargetFrameworks>
4+
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)</TargetFrameworks>
55
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
66
<!-- SYSTEM_NET_SOCKETS_DLL is required to allow source-level code sharing for types defined within the
77
System.Net.Internals namespace. -->
@@ -13,6 +13,8 @@
1313
<PropertyGroup>
1414
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
1515
<GeneratePlatformNotSupportedAssemblyMessage Condition="'$(TargetPlatformIdentifier)' == ''">SR.SystemNetSockets_PlatformNotSupported</GeneratePlatformNotSupportedAssemblyMessage>
16+
<IsApplePlatform Condition="'$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">true</IsApplePlatform>
17+
<DefineConstants Condition="'$(IsApplePlatform)' == 'true'">$(DefineConstants);SYSTEM_NET_SOCKETS_APPLE_PLATFROM</DefineConstants>
1618
</PropertyGroup>
1719

1820
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != ''">
@@ -181,7 +183,7 @@
181183
Link="Common\System\Net\CompletionPortHelper.Windows.cs" />
182184
</ItemGroup>
183185

184-
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'unix'">
186+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'unix' or '$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">
185187
<Compile Include="System\Net\Sockets\SafeSocketHandle.Unix.cs" />
186188
<Compile Include="System\Net\Sockets\Socket.Unix.cs" />
187189
<Compile Include="System\Net\Sockets\SocketAsyncContext.Unix.cs" />
@@ -301,7 +303,7 @@
301303
<Reference Include="System.Threading.ThreadPool" />
302304
</ItemGroup>
303305

304-
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'unix'">
306+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'windows'">
305307
<Reference Include="System.Threading.Thread" />
306308
</ItemGroup>
307309

src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ public partial class SafeSocketHandle
2323
internal bool ExposedHandleOrUntrackedConfiguration { get; private set; }
2424
internal bool PreferInlineCompletions { get; set; } = SocketAsyncEngine.InlineSocketCompletionsEnabled;
2525
internal bool IsSocket { get; set; } = true; // (ab)use Socket class for performing async I/O on non-socket fds.
26-
26+
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
27+
internal bool TfoEnabled { get; set; }
28+
#endif
2729
internal void RegisterConnectResult(SocketError error)
2830
{
2931
switch (error)
@@ -44,6 +46,9 @@ internal void TransferTrackedState(SafeSocketHandle target)
4446
target.DualMode = DualMode;
4547
target.ExposedHandleOrUntrackedConfiguration = ExposedHandleOrUntrackedConfiguration;
4648
target.IsSocket = IsSocket;
49+
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
50+
target.TfoEnabled = TfoEnabled;
51+
#endif
4752
}
4853

4954
internal void SetExposed() => ExposedHandleOrUntrackedConfiguration = true;

src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ public override unsafe void InvokeCallback(bool allowPooling) =>
362362
Callback!(BytesTransferred, SocketAddress, SocketFlags.None, ErrorCode);
363363
}
364364

365-
private sealed class BufferMemorySendOperation : SendOperation
365+
private class BufferMemorySendOperation : SendOperation
366366
{
367367
public Memory<byte> Buffer;
368368

@@ -648,21 +648,47 @@ public override void InvokeCallback(bool allowPooling)
648648
}
649649
}
650650

651-
private sealed class ConnectOperation : WriteOperation
651+
private sealed class ConnectOperation : BufferMemorySendOperation
652652
{
653653
public ConnectOperation(SocketAsyncContext context) : base(context) { }
654654

655-
public Action<SocketError>? Callback { get; set; }
656-
657655
protected override bool DoTryComplete(SocketAsyncContext context)
658656
{
659657
bool result = SocketPal.TryCompleteConnect(context._socket, out ErrorCode);
660658
context._socket.RegisterConnectResult(ErrorCode);
659+
660+
if (result && ErrorCode == SocketError.Success && Buffer.Length > 0)
661+
{
662+
SocketError error = context.SendToAsync(Buffer, 0, Buffer.Length, SocketFlags.None, Memory<byte>.Empty, ref BytesTransferred, Callback!, default);
663+
if (error != SocketError.Success && error != SocketError.IOPending)
664+
{
665+
context._socket.RegisterConnectResult(ErrorCode);
666+
}
667+
}
661668
return result;
662669
}
663670

664-
public override void InvokeCallback(bool allowPooling) =>
665-
Callback!(ErrorCode);
671+
public override unsafe void InvokeCallback(bool allowPooling)
672+
{
673+
var cb = Callback!;
674+
int bt = BytesTransferred;
675+
Memory<byte> sa = SocketAddress;
676+
SocketError ec = ErrorCode;
677+
Memory<byte> buffer = Buffer;
678+
679+
if (allowPooling)
680+
{
681+
AssociatedContext.ReturnOperation(this);
682+
}
683+
684+
if (buffer.Length == 0)
685+
{
686+
// Invoke callback only when we are completely done.
687+
// In case data were provided for Connect we may or may not send them all.
688+
// If we did not we will need follow-up with Send operation
689+
cb(bt, sa, SocketFlags.None, ec);
690+
}
691+
}
666692
}
667693

668694
private sealed class SendFileOperation : WriteOperation
@@ -1478,7 +1504,6 @@ public SocketError AcceptAsync(Memory<byte> socketAddress, out int socketAddress
14781504
public SocketError Connect(Memory<byte> socketAddress)
14791505
{
14801506
Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}");
1481-
14821507
// Connect is different than the usual "readiness" pattern of other operations.
14831508
// We need to call TryStartConnect to initiate the connect with the OS,
14841509
// before we try to complete it via epoll notification.
@@ -1503,7 +1528,7 @@ public SocketError Connect(Memory<byte> socketAddress)
15031528
return operation.ErrorCode;
15041529
}
15051530

1506-
public SocketError ConnectAsync(Memory<byte> socketAddress, Action<SocketError> callback)
1531+
public SocketError ConnectAsync(Memory<byte> socketAddress, Action<int, Memory<byte>, SocketFlags, SocketError> callback, Memory<byte> buffer, out int sentBytes)
15071532
{
15081533
Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}");
15091534
Debug.Assert(callback != null, "Expected non-null callback");
@@ -1516,20 +1541,37 @@ public SocketError ConnectAsync(Memory<byte> socketAddress, Action<SocketError>
15161541
SocketError errorCode;
15171542
int observedSequenceNumber;
15181543
_sendQueue.IsReady(this, out observedSequenceNumber);
1519-
if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode))
1544+
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
1545+
if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode, buffer.Span, _socket.TfoEnabled, out sentBytes))
1546+
#else
1547+
if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode, buffer.Span, false, out sentBytes)) // In Linux, we can figure it out as needed inside PAL.
1548+
#endif
15201549
{
15211550
_socket.RegisterConnectResult(errorCode);
1551+
1552+
int remains = buffer.Length - sentBytes;
1553+
1554+
if (errorCode == SocketError.Success && remains > 0)
1555+
{
1556+
errorCode = SendToAsync(buffer.Slice(sentBytes), 0, remains, SocketFlags.None, Memory<byte>.Empty, ref sentBytes, callback!, default);
1557+
}
15221558
return errorCode;
15231559
}
15241560

15251561
var operation = new ConnectOperation(this)
15261562
{
15271563
Callback = callback,
15281564
SocketAddress = socketAddress,
1565+
Buffer = buffer.Slice(sentBytes),
1566+
BytesTransferred = sentBytes,
15291567
};
15301568

15311569
if (!_sendQueue.StartAsyncOperation(this, operation, observedSequenceNumber))
15321570
{
1571+
if (operation.ErrorCode == SocketError.Success)
1572+
{
1573+
sentBytes += operation.BytesTransferred;
1574+
}
15331575
return operation.ErrorCode;
15341576
}
15351577

@@ -1880,7 +1922,8 @@ public SocketError Send(byte[] buffer, int offset, int count, SocketFlags flags,
18801922

18811923
public SocketError SendAsync(Memory<byte> buffer, int offset, int count, SocketFlags flags, out int bytesSent, Action<int, Memory<byte>, SocketFlags, SocketError> callback, CancellationToken cancellationToken)
18821924
{
1883-
return SendToAsync(buffer, offset, count, flags, Memory<byte>.Empty, out bytesSent, callback, cancellationToken);
1925+
bytesSent = 0;
1926+
return SendToAsync(buffer, offset, count, flags, Memory<byte>.Empty, ref bytesSent, callback, cancellationToken);
18841927
}
18851928

18861929
public SocketError SendTo(byte[] buffer, int offset, int count, SocketFlags flags, Memory<byte> socketAddress, int timeout, out int bytesSent)
@@ -1947,11 +1990,10 @@ public unsafe SocketError SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, M
19471990
}
19481991
}
19491992

1950-
public SocketError SendToAsync(Memory<byte> buffer, int offset, int count, SocketFlags flags, Memory<byte> socketAddress, out int bytesSent, Action<int, Memory<byte>, SocketFlags, SocketError> callback, CancellationToken cancellationToken = default)
1993+
public SocketError SendToAsync(Memory<byte> buffer, int offset, int count, SocketFlags flags, Memory<byte> socketAddress, ref int bytesSent, Action<int, Memory<byte>, SocketFlags, SocketError> callback, CancellationToken cancellationToken = default)
19511994
{
19521995
SetHandleNonBlocking();
19531996

1954-
bytesSent = 0;
19551997
SocketError errorCode;
19561998
int observedSequenceNumber;
19571999
if (_sendQueue.IsReady(this, out observedSequenceNumber) &&

src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Unix.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,24 @@ internal unsafe SocketError DoOperationAccept(Socket _ /*socket*/, SafeSocketHan
6767
return socketError;
6868
}
6969

70-
private void ConnectCompletionCallback(SocketError socketError)
70+
private void ConnectCompletionCallback(int bytesTransferred, Memory<byte> socketAddress, SocketFlags receivedFlags, SocketError socketError)
7171
{
72-
CompletionCallback(0, SocketFlags.None, socketError);
72+
CompletionCallback(bytesTransferred, SocketFlags.None, socketError);
7373
}
7474

7575
internal unsafe SocketError DoOperationConnectEx(Socket _ /*socket*/, SafeSocketHandle handle)
76-
=> DoOperationConnect(handle);
76+
{
77+
SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback, _buffer.Slice(_offset, _count), out int sentBytes);
78+
if (socketError != SocketError.IOPending)
79+
{
80+
FinishOperationSync(socketError, sentBytes, SocketFlags.None);
81+
}
82+
return socketError;
83+
}
7784

7885
internal unsafe SocketError DoOperationConnect(SafeSocketHandle handle)
7986
{
80-
SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback);
87+
SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback, Memory<byte>.Empty, out int _);
8188
if (socketError != SocketError.IOPending)
8289
{
8390
FinishOperationSync(socketError, 0, SocketFlags.None);
@@ -299,11 +306,11 @@ internal SocketError DoOperationSendTo(SafeSocketHandle handle, CancellationToke
299306
_receivedFlags = System.Net.Sockets.SocketFlags.None;
300307
_socketAddressSize = 0;
301308

302-
int bytesSent;
309+
int bytesSent = 0;
303310
SocketError errorCode;
304311
if (_bufferList == null)
305312
{
306-
errorCode = handle.AsyncContext.SendToAsync(_buffer, _offset, _count, _socketFlags, _socketAddress!.Buffer, out bytesSent, TransferCompletionCallback, cancellationToken);
313+
errorCode = handle.AsyncContext.SendToAsync(_buffer, _offset, _count, _socketFlags, _socketAddress!.Buffer, ref bytesSent, TransferCompletionCallback, cancellationToken);
307314
}
308315
else
309316
{

src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketOptionName.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,34 @@ public enum SocketOptionName
132132
#endregion
133133

134134
#region SocketOptionLevel.Tcp
135-
// Disables the Nagle algorithm for send coalescing.
135+
/// <summary>
136+
/// Disables the Nagle algorithm for send coalescing.
137+
/// </summary>
136138
NoDelay = 1,
139+
/// <summary>
140+
/// Use urgent data as defined in RFC-1222. This option can be set only once; after it is set, it cannot be turned off.
141+
/// </summary>
137142
BsdUrgent = 2,
143+
/// <summary>
144+
/// Use expedited data as defined in RFC-1222. This option can be set only once; after it is set, it cannot be turned off.
145+
/// </summary>
138146
Expedited = 2,
147+
/// <summary>
148+
/// This enables TCP Fast Open as defined in RFC-7413. The actual observed behavior depend on OS configuration and state of kernel TCP cookie cache.
149+
/// Enabling TFO can impact interoperability and casue connectivity issues.
150+
/// </summary>
151+
FastOpen = 15,
152+
/// <summary>
153+
/// The number of TCP keep alive probes that will be sent before the connection is terminated.
154+
/// </summary>
139155
TcpKeepAliveRetryCount = 16,
156+
/// <summary>
157+
/// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote.
158+
/// </summary>
140159
TcpKeepAliveTime = 3,
160+
/// <summary>
161+
/// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe.
162+
/// </summary>
141163
TcpKeepAliveInterval = 17,
142164
#endregion
143165

src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,9 +647,12 @@ public static unsafe bool TryCompleteAccept(SafeSocketHandle socket, Memory<byte
647647
return false;
648648
}
649649

650-
public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory<byte> socketAddress, out SocketError errorCode)
650+
public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory<byte> socketAddress, out SocketError errorCode) => TryStartConnect(socket, socketAddress, out errorCode, Span<byte>.Empty, false, out int _ );
651+
652+
public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory<byte> socketAddress, out SocketError errorCode, Span<byte> data, bool tfo, out int sent)
651653
{
652654
Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}");
655+
sent = 0;
653656

654657
if (socket.IsDisconnected)
655658
{
@@ -660,7 +663,16 @@ public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory<byte>
660663
Interop.Error err;
661664
fixed (byte* rawSocketAddress = socketAddress.Span)
662665
{
663-
err = Interop.Sys.Connect(socket, rawSocketAddress, socketAddress.Length);
666+
if (data.Length > 0)
667+
{
668+
int sentBytes = 0;
669+
err = Interop.Sys.Connectx(socket, rawSocketAddress, socketAddress.Length, data, data.Length, tfo ? 1 : 0, &sentBytes);
670+
sent = sentBytes;
671+
}
672+
else
673+
{
674+
err = Interop.Sys.Connect(socket, rawSocketAddress, socketAddress.Length);
675+
}
664676
}
665677

666678
if (err == Interop.Error.SUCCESS)
@@ -1451,6 +1463,18 @@ public static unsafe SocketError SetSockOpt(SafeSocketHandle handle, SocketOptio
14511463
}
14521464
}
14531465

1466+
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
1467+
// macOS fails to even query it if socket is not actively listening.
1468+
// To provide consistent platform experience we will track if
1469+
// it was ret and we will use it later as needed.
1470+
if (optionLevel == SocketOptionLevel.Tcp && optionName == SocketOptionName.FastOpen)
1471+
{
1472+
handle.TfoEnabled = optionValue != 0;
1473+
// Silently ignore errors - TFO is best effort and it may be disabled by configuration or not
1474+
// supported by OS.
1475+
err = Interop.Error.SUCCESS;
1476+
}
1477+
#endif
14541478
return GetErrorAndTrackSetting(handle, optionLevel, optionName, err);
14551479
}
14561480

@@ -1580,6 +1604,17 @@ public static unsafe SocketError GetSockOpt(SafeSocketHandle handle, SocketOptio
15801604
int optLen = sizeof(int);
15811605
Interop.Error err = Interop.Sys.GetSockOpt(handle, optionLevel, optionName, (byte*)&value, &optLen);
15821606

1607+
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
1608+
// macOS fails to even query it if socket is not actively listening.
1609+
// To provide consistent platform experience we will track if
1610+
// it was set and we will use it later as needed.
1611+
if (optionLevel == SocketOptionLevel.Tcp && optionName == SocketOptionName.FastOpen && err != Interop.Error.SUCCESS)
1612+
{
1613+
value = handle.TfoEnabled ? 1 : 0;
1614+
err = Interop.Error.SUCCESS;
1615+
}
1616+
#endif
1617+
15831618
optionValue = value;
15841619
return err == Interop.Error.SUCCESS ? SocketError.Success : GetSocketErrorForErrorCode(err);
15851620
}

src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,6 @@ public sealed class ConnectEap : Connect<SocketHelperEap>
266266
public ConnectEap(ITestOutputHelper output) : base(output) {}
267267

268268
[Theory]
269-
[PlatformSpecific(TestPlatforms.Windows)]
270269
[InlineData(true)]
271270
[InlineData(false)]
272271
public async Task ConnectAsync_WithData_DataReceived(bool useArrayApi)

0 commit comments

Comments
 (0)