diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
index 5dfca52bb9..74f3ef6889 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
@@ -87,9 +87,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
_localEndPoint = localEndPoint;
_prepareRequest = prepareRequest;
_pathBase = context.ServerAddress.PathBase;
- _requestBody = new FrameRequestStream();
- _responseBody = new FrameResponseStream(this);
- _duplexStream = new FrameDuplexStream(_requestBody, _responseBody);
+ if (ReuseStreams)
+ {
+ _requestBody = new FrameRequestStream();
+ _responseBody = new FrameResponseStream(this);
+ _duplexStream = new FrameDuplexStream(_requestBody, _responseBody);
+ }
FrameControl = this;
Reset();
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs
index d9afe93d92..32b067fc76 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs
@@ -64,6 +64,15 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
var messageBody = MessageBody.For(HttpVersion, _requestHeaders, this);
_keepAlive = messageBody.RequestKeepAlive;
+
+ // _duplexStream may be null if flag switched while running
+ if (!ReuseStreams || _duplexStream == null)
+ {
+ _requestBody = new FrameRequestStream();
+ _responseBody = new FrameResponseStream(this);
+ _duplexStream = new FrameDuplexStream(_requestBody, _responseBody);
+ }
+
RequestBody = _requestBody.StartAcceptingReads(messageBody);
ResponseBody = _responseBody.StartAcceptingWrites();
DuplexStream = _duplexStream;
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs
index 0c8ea64e81..f9ff5a4197 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs
@@ -11,11 +11,11 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public class FrameRequestStream : Stream
{
private MessageBody _body;
- private StreamState _state;
+ private FrameStreamState _state;
public FrameRequestStream()
{
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
}
public override bool CanRead { get { return true; } }
@@ -115,9 +115,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public Stream StartAcceptingReads(MessageBody body)
{
// Only start if not aborted
- if (_state == StreamState.Closed)
+ if (_state == FrameStreamState.Closed)
{
- _state = StreamState.Open;
+ _state = FrameStreamState.Open;
_body = body;
}
return this;
@@ -125,14 +125,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public void PauseAcceptingReads()
{
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
}
public void ResumeAcceptingReads()
{
- if (_state == StreamState.Closed)
+ if (_state == FrameStreamState.Closed)
{
- _state = StreamState.Open;
+ _state = FrameStreamState.Open;
}
}
@@ -140,7 +140,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
// Can't use dispose (or close) as can be disposed too early by user code
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
_body = null;
}
@@ -148,9 +148,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
// We don't want to throw an ODE until the app func actually completes.
// If the request is aborted, we throw an IOException instead.
- if (_state != StreamState.Closed)
+ if (_state != FrameStreamState.Closed)
{
- _state = StreamState.Aborted;
+ _state = FrameStreamState.Aborted;
}
}
@@ -158,20 +158,13 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
switch (_state)
{
- case StreamState.Open:
+ case FrameStreamState.Open:
return;
- case StreamState.Closed:
+ case FrameStreamState.Closed:
throw new ObjectDisposedException(nameof(FrameRequestStream));
- case StreamState.Aborted:
+ case FrameStreamState.Aborted:
throw new IOException("The request has been aborted.");
}
}
-
- private enum StreamState
- {
- Open,
- Closed,
- Aborted
- }
}
}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs
index 9d0658f89b..e544723af4 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs
@@ -11,12 +11,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
class FrameResponseStream : Stream
{
private readonly FrameContext _context;
- private StreamState _state;
+ private FrameStreamState _state;
public FrameResponseStream(FrameContext context)
{
_context = context;
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
}
public override bool CanRead => false;
@@ -81,9 +81,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public Stream StartAcceptingWrites()
{
// Only start if not aborted
- if (_state == StreamState.Closed)
+ if (_state == FrameStreamState.Closed)
{
- _state = StreamState.Open;
+ _state = FrameStreamState.Open;
}
return this;
@@ -91,14 +91,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public void PauseAcceptingWrites()
{
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
}
public void ResumeAcceptingWrites()
{
- if (_state == StreamState.Closed)
+ if (_state == FrameStreamState.Closed)
{
- _state = StreamState.Open;
+ _state = FrameStreamState.Open;
}
}
@@ -106,16 +106,16 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
// Can't use dispose (or close) as can be disposed too early by user code
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
- _state = StreamState.Closed;
+ _state = FrameStreamState.Closed;
}
public void Abort()
{
// We don't want to throw an ODE until the app func actually completes.
// If the request is aborted, we throw an IOException instead.
- if (_state != StreamState.Closed)
+ if (_state != FrameStreamState.Closed)
{
- _state = StreamState.Aborted;
+ _state = FrameStreamState.Aborted;
}
}
@@ -123,20 +123,13 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
switch (_state)
{
- case StreamState.Open:
+ case FrameStreamState.Open:
return;
- case StreamState.Closed:
+ case FrameStreamState.Closed:
throw new ObjectDisposedException(nameof(FrameResponseStream));
- case StreamState.Aborted:
+ case FrameStreamState.Aborted:
throw new IOException("The request has been aborted.");
}
}
-
- private enum StreamState
- {
- Open,
- Closed,
- Aborted
- }
}
}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs
new file mode 100644
index 0000000000..12c393ee1a
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNet.Server.Kestrel.Http
+{
+ enum FrameStreamState
+ {
+ Open,
+ Closed,
+ Aborted
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs b/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs
index 870fe2e533..3a634afae8 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs
@@ -11,6 +11,19 @@ namespace Microsoft.AspNet.Server.Kestrel
bool NoDelay { get; set; }
+ ///
+ /// Gets or sets a flag that instructs whether it is safe to
+ /// reuse the Request and Response objects
+ /// for another request after the Response's OnCompleted callback has fired.
+ /// When this is set to true it is not safe to retain references to these streams after this event has fired.
+ /// It is false by default.
+ ///
+ ///
+ /// When this is set to true it is not safe to retain references to these streams after this event has fired.
+ /// It is false by default.
+ ///
+ bool ReuseStreams { get; set; }
+
IConnectionFilter ConnectionFilter { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs
index 3a1295de76..34cef6394b 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs
@@ -67,7 +67,8 @@ namespace Microsoft.AspNet.Server.Kestrel
ThreadPool = new LoggingThreadPool(trace),
DateHeaderValueManager = dateHeaderValueManager,
ConnectionFilter = information.ConnectionFilter,
- NoDelay = information.NoDelay
+ NoDelay = information.NoDelay,
+ ReuseStreams = information.ReuseStreams
});
_disposables.Push(engine);
diff --git a/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs b/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs
index f9f4ae8c8e..8ff73cc03e 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs
@@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Server.Kestrel
Addresses = GetAddresses(configuration);
ThreadCount = GetThreadCount(configuration);
NoDelay = GetNoDelay(configuration);
+ ReuseStreams = GetReuseStreams(configuration);
}
public ICollection Addresses { get; }
@@ -30,6 +31,8 @@ namespace Microsoft.AspNet.Server.Kestrel
public bool NoDelay { get; set; }
+ public bool ReuseStreams { get; set; }
+
public IConnectionFilter ConnectionFilter { get; set; }
private static int ProcessorThreadCount
@@ -107,5 +110,18 @@ namespace Microsoft.AspNet.Server.Kestrel
return true;
}
+
+ private static bool GetReuseStreams(IConfiguration configuration)
+ {
+ var reuseStreamsString = configuration["kestrel.reuseStreams"];
+
+ bool reuseStreams;
+ if (bool.TryParse(reuseStreamsString, out reuseStreams))
+ {
+ return reuseStreams;
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
index 31eeea898d..4bab512ced 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
@@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Server.Kestrel
DateHeaderValueManager = context.DateHeaderValueManager;
ConnectionFilter = context.ConnectionFilter;
NoDelay = context.NoDelay;
+ ReuseStreams = context.ReuseStreams;
}
public IApplicationLifetime AppLifetime { get; set; }
@@ -41,5 +42,7 @@ namespace Microsoft.AspNet.Server.Kestrel
public IConnectionFilter ConnectionFilter { get; set; }
public bool NoDelay { get; set; }
+
+ public bool ReuseStreams { get; set; }
}
}
diff --git a/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs
new file mode 100644
index 0000000000..d61a2759e2
--- /dev/null
+++ b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs
@@ -0,0 +1,127 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Hosting;
+using Microsoft.AspNet.Http.Features;
+using Microsoft.Extensions.Configuration;
+using Xunit;
+
+namespace Microsoft.AspNet.Server.Kestrel.FunctionalTests
+{
+ public class ReuseStreamsTests
+ {
+ [Fact]
+ public async Task ReuseStreamsOn()
+ {
+ var streamCount = 0;
+ var loopCount = 20;
+ Stream lastStream = null;
+ var config = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ { "server.urls", "http://localhost:8801/" },
+ { "kestrel.reuseStreams", "true" }
+ })
+ .Build();
+
+ var hostBuilder = new WebHostBuilder(config);
+ hostBuilder.UseServerFactory("Microsoft.AspNet.Server.Kestrel");
+ hostBuilder.UseStartup(app =>
+ {
+ var serverInfo = app.ServerFeatures.Get();
+ app.Run(context =>
+ {
+ if (context.Request.Body != lastStream)
+ {
+ lastStream = context.Request.Body;
+ streamCount++;
+ }
+ return context.Request.Body.CopyToAsync(context.Response.Body);
+ });
+ });
+
+ using (var app = hostBuilder.Build().Start())
+ {
+ using (var client = new HttpClient())
+ {
+ for (int i = 0; i < loopCount; i++)
+ {
+ var content = $"{i} Hello World {i}";
+ var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri("http://localhost:8801/"),
+ Method = HttpMethod.Post,
+ Content = new StringContent(content)
+ };
+ request.Headers.Add("Connection", new string[] { "Keep-Alive" });
+ var responseMessage = await client.SendAsync(request);
+ var result = await responseMessage.Content.ReadAsStringAsync();
+ Assert.Equal(content, result);
+ }
+ }
+ }
+
+ Assert.True(streamCount < loopCount);
+ }
+
+ [Fact]
+ public async Task ReuseStreamsOff()
+ {
+ var streamCount = 0;
+ var loopCount = 20;
+ Stream lastStream = null;
+ var config = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ { "server.urls", "http://localhost:8802/" },
+ { "kestrel.reuseStreams", "false" }
+ })
+ .Build();
+
+ var hostBuilder = new WebHostBuilder(config);
+ hostBuilder.UseServerFactory("Microsoft.AspNet.Server.Kestrel");
+ hostBuilder.UseStartup(app =>
+ {
+ var serverInfo = app.ServerFeatures.Get();
+ app.Run(context =>
+ {
+ if (context.Request.Body != lastStream)
+ {
+ lastStream = context.Request.Body;
+ streamCount++;
+ }
+ return context.Request.Body.CopyToAsync(context.Response.Body);
+ });
+ });
+
+ using (var app = hostBuilder.Build().Start())
+ {
+ using (var client = new HttpClient())
+ {
+ for (int i = 0; i < loopCount; i++)
+ {
+ var content = $"{i} Hello World {i}";
+ var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri("http://localhost:8802/"),
+ Method = HttpMethod.Post,
+ Content = new StringContent(content)
+ };
+ request.Headers.Add("Connection", new string[] { "Keep-Alive" });
+ var responseMessage = await client.SendAsync(request);
+ var result = await responseMessage.Content.ReadAsStringAsync();
+ Assert.Equal(content, result);
+ }
+ }
+ }
+
+ Assert.Equal(loopCount, streamCount);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs
index 2513d93e84..e2376c0d94 100644
--- a/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs
+++ b/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs
@@ -85,6 +85,31 @@ namespace Microsoft.AspNet.Server.KestrelTests
Assert.False(information.NoDelay);
}
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData("false", false)]
+ [InlineData("False", false)]
+ [InlineData("true", true)]
+ [InlineData("True", true)]
+ [InlineData("Foo", false)]
+ [InlineData("FooBar", false)]
+ public void SetReuseStreamsUsingConfiguration(string input, bool expected)
+ {
+ var values = new Dictionary
+ {
+ { "kestrel.reuseStreams", input }
+ };
+
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(values)
+ .Build();
+
+ var information = new KestrelServerInformation(configuration);
+
+ Assert.Equal(expected, information.ReuseStreams);
+ }
+
private static int Clamp(int value, int min, int max)
{
return value < min ? min : value > max ? max : value;