Skip to content

Commit

Permalink
Implement TCP Keep-Alive for WinHttpHandler (#44889)
Browse files Browse the repository at this point in the history
Implements the final version of the API proposal in #44025 except the [SupportedOSPlatform("windows10.0.2004")] bits
  • Loading branch information
antonfirsov committed Nov 25, 2020
1 parent 0262b49 commit 837785f
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ internal partial class WinHttp
public const uint WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE = 122;
public const uint WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE = 123;

public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152;

public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE
{
WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0,
Expand Down Expand Up @@ -276,6 +278,15 @@ public struct WINHTTP_ASYNC_RESULT
public uint dwError;
}


[StructLayout(LayoutKind.Sequential)]
public struct tcp_keepalive
{
public uint onoff;
public uint keepalivetime;
public uint keepaliveinterval;
}

public const uint API_RECEIVE_RESPONSE = 1;
public const uint API_QUERY_DATA_AVAILABLE = 2;
public const uint API_READ_DATA = 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public partial class WinHttpHandler : System.Net.Http.HttpMessageHandler
public System.Func<System.Net.Http.HttpRequestMessage, System.Security.Cryptography.X509Certificates.X509Certificate2, System.Security.Cryptography.X509Certificates.X509Chain, System.Net.Security.SslPolicyErrors, bool>? ServerCertificateValidationCallback { get { throw null; } set { } }
public System.Net.ICredentials? ServerCredentials { get { throw null; } set { } }
public System.Security.Authentication.SslProtocols SslProtocols { get { throw null; } set { } }
public bool TcpKeepAliveEnabled { get { throw null; } set { } }
public System.TimeSpan TcpKeepAliveTime { get { throw null; } set { } }
public System.TimeSpan TcpKeepAliveInterval { get { throw null; } set { } }
public System.Net.Http.WindowsProxyUsePolicy WindowsProxyUsePolicy { get { throw null; } set { } }
protected override void Dispose(bool disposing) { }
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ public class WinHttpHandler : HttpMessageHandler
private TimeSpan _sendTimeout = TimeSpan.FromSeconds(30);
private TimeSpan _receiveHeadersTimeout = TimeSpan.FromSeconds(30);
private TimeSpan _receiveDataTimeout = TimeSpan.FromSeconds(30);

// Using OS defaults for "Keep-alive timeout" and "keep-alive interval"
// as documented in https://docs.microsoft.com/en-us/windows/win32/winsock/sio-keepalive-vals#remarks
private TimeSpan _tcpKeepAliveTime = TimeSpan.FromHours(2);
private TimeSpan _tcpKeepAliveInterval = TimeSpan.FromSeconds(1);
private bool _tcpKeepAliveEnabled;

private int _maxResponseHeadersLength = HttpHandlerDefaults.DefaultMaxResponseHeadersLength;
private int _maxResponseDrainSize = 64 * 1024;
private IDictionary<string, object> _properties; // Only create dictionary when required.
Expand Down Expand Up @@ -188,6 +195,7 @@ public SslProtocols SslProtocols
}
}


public Func<
HttpRequestMessage,
X509Certificate2,
Expand Down Expand Up @@ -369,16 +377,13 @@ public TimeSpan SendTimeout

set
{
if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout))
{
throw new ArgumentOutOfRangeException(nameof(value));
}

CheckTimeSpanPropertyValue(value);
CheckDisposedOrStarted();
_sendTimeout = value;
}
}


public TimeSpan ReceiveHeadersTimeout
{
get
Expand All @@ -388,11 +393,7 @@ public TimeSpan ReceiveHeadersTimeout

set
{
if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout))
{
throw new ArgumentOutOfRangeException(nameof(value));
}

CheckTimeSpanPropertyValue(value);
CheckDisposedOrStarted();
_receiveHeadersTimeout = value;
}
Expand All @@ -407,16 +408,74 @@ public TimeSpan ReceiveDataTimeout

set
{
if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout))
{
throw new ArgumentOutOfRangeException(nameof(value));
}

CheckTimeSpanPropertyValue(value);
CheckDisposedOrStarted();
_receiveDataTimeout = value;
}
}

/// <summary>
/// Gets or sets a value indicating whether TCP keep-alive is enabled.
/// </summary>
/// <remarks>
/// If enabled, the values of <see cref="TcpKeepAliveInterval" /> and <see cref="TcpKeepAliveTime"/> will be forwarded
/// to set WINHTTP_OPTION_TCP_KEEPALIVE, enabling and configuring TCP keep-alive for the backing TCP socket.
/// </remarks>
public bool TcpKeepAliveEnabled
{
get
{
return _tcpKeepAliveEnabled;
}
set
{
CheckDisposedOrStarted();
_tcpKeepAliveEnabled = value;
}
}

/// <summary>
/// Gets or sets the TCP keep-alive timeout.
/// </summary>
/// <remarks>
/// Has no effect if <see cref="TcpKeepAliveEnabled"/> is <see langword="false" />.
/// The default value of this property is 2 hours.
/// </remarks>
public TimeSpan TcpKeepAliveTime
{
get
{
return _tcpKeepAliveTime;
}
set
{
CheckTimeSpanPropertyValue(value);
CheckDisposedOrStarted();
_tcpKeepAliveTime = value;
}
}

/// <summary>
/// Gets or sets the TCP keep-alive interval.
/// </summary>
/// <remarks>
/// Has no effect if <see cref="TcpKeepAliveEnabled"/> is <see langword="false" />.
/// The default value of this property is 1 second.
/// </remarks>
public TimeSpan TcpKeepAliveInterval
{
get
{
return _tcpKeepAliveInterval;
}
set
{
CheckTimeSpanPropertyValue(value);
CheckDisposedOrStarted();
_tcpKeepAliveInterval = value;
}
}

public int MaxResponseHeadersLength
{
get
Expand Down Expand Up @@ -936,6 +995,28 @@ private void SetSessionHandleOptions(SafeWinHttpHandle sessionHandle)
SetSessionHandleTlsOptions(sessionHandle);
SetSessionHandleTimeoutOptions(sessionHandle);
SetDisableHttp2StreamQueue(sessionHandle);
SetTcpKeepalive(sessionHandle);
}

private unsafe void SetTcpKeepalive(SafeWinHttpHandle sessionHandle)
{
if (_tcpKeepAliveEnabled)
{
var tcpKeepalive = new Interop.WinHttp.tcp_keepalive
{
onoff = 1,

// Timeout.InfiniteTimeSpan will be converted to uint.MaxValue milliseconds (~ 50 days)
keepaliveinterval = (uint)_tcpKeepAliveInterval.TotalMilliseconds,
keepalivetime = (uint)_tcpKeepAliveTime.TotalMilliseconds
};

SetWinHttpOption(
sessionHandle,
Interop.WinHttp.WINHTTP_OPTION_TCP_KEEPALIVE,
(IntPtr)(&tcpKeepalive),
(uint)sizeof(Interop.WinHttp.tcp_keepalive));
}
}

private void SetSessionHandleConnectionOptions(SafeWinHttpHandle sessionHandle)
Expand Down Expand Up @@ -1363,6 +1444,14 @@ private void CheckDisposedOrStarted()
}
}

private static void CheckTimeSpanPropertyValue(TimeSpan timeSpan)
{
if (timeSpan != Timeout.InfiniteTimeSpan && (timeSpan <= TimeSpan.Zero || timeSpan > s_maxTimeout))
{
throw new ArgumentOutOfRangeException("value");
}
}

private void SetStatusCallback(
SafeWinHttpHandle requestHandle,
Interop.WinHttp.WINHTTP_STATUS_CALLBACK callback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ public async Task GetAsync_SetCookieContainerMultipleCookies_CookiesSent()
Assert.Equal("POST", responseContent.Method);
Assert.Equal(payload, responseContent.BodyContent);
Assert.Equal(cookies.ToDictionary(c => c.Name, c => c.Value), responseContent.Cookies);

};
}

Expand Down Expand Up @@ -218,6 +217,28 @@ public async Task SendAsync_MultipleHttp2ConnectionsEnabled_CreateAdditionalConn
}
}

[OuterLoop("Uses external service")]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows10Version2004OrGreater))]
public async Task SendAsync_UseTcpKeepAliveOptions()
{
using var handler = new WinHttpHandler()
{
TcpKeepAliveEnabled = true,
TcpKeepAliveTime = TimeSpan.FromSeconds(1),
TcpKeepAliveInterval = TimeSpan.FromMilliseconds(500)
};

using var client = new HttpClient(handler);

var response = client.GetAsync(System.Net.Test.Common.Configuration.Http.RemoteEchoServer).Result;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string responseContent = await response.Content.ReadAsStringAsync();
_output.WriteLine(responseContent);

// Uncomment this to observe an exchange of "TCP Keep-Alive" and "TCP Keep-Alive ACK" packets:
// await Task.Delay(5000);
}

private async Task VerifyResponse(Task<HttpResponseMessage> task, string payloadText)
{
Assert.True(task.IsCompleted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public static ProxyInfo RequestProxySettings

public static List<IntPtr> WinHttpOptionClientCertContext { get { return winHttpOptionClientCertContextList; } }

public static (uint OnOff, uint KeepAliveTime, uint KeepAliveInterval)? WinHttpOptionTcpKeepAlive { get; set; }

public static void Reset()
{
sessionProxySettings.AccessType = null;
Expand All @@ -93,6 +95,7 @@ public static void Reset()
WinHttpOptionRedirectPolicy = null;
WinHttpOptionSendTimeout = null;
WinHttpOptionReceiveTimeout = null;
WinHttpOptionTcpKeepAlive = null;
winHttpOptionClientCertContextList.Clear();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ private static bool CopyToBufferOrFailIfInsufficientBufferLength(string value, I
return true;
}

public static bool WinHttpSetOption(
public unsafe static bool WinHttpSetOption(
SafeWinHttpHandle handle,
uint option,
IntPtr optionData,
Expand All @@ -556,6 +556,11 @@ private static bool CopyToBufferOrFailIfInsufficientBufferLength(string value, I
{
APICallHistory.WinHttpOptionClientCertContext.Add(optionData);
}
else if (option == Interop.WinHttp.WINHTTP_OPTION_TCP_KEEPALIVE)
{
Interop.WinHttp.tcp_keepalive* ptr = (Interop.WinHttp.tcp_keepalive*)optionData;
APICallHistory.WinHttpOptionTcpKeepAlive = (ptr->onoff, ptr->keepalivetime, ptr->keepaliveinterval);
}

return true;
}
Expand Down
Loading

0 comments on commit 837785f

Please sign in to comment.