Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TCP Keep-Alive for WinHttpHandler #44889

Merged
merged 7 commits into from
Nov 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
scalablecory marked this conversation as resolved.
Show resolved Hide resolved
};

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