From 5faf814c2f4695539fcc35c8c50e22782a3f4e41 Mon Sep 17 00:00:00 2001 From: Pieter van Ginkel Date: Sat, 5 Dec 2015 17:15:40 +0100 Subject: [PATCH] Implemented timeouts. Implemented timeouts on reading from and writing to a TCP connection. Bug: #5 --- NHttp/HttpClient.cs | 19 ++++- NHttp/HttpReadBuffer.cs | 4 +- NHttp/HttpServer.cs | 16 +++++ NHttp/HttpTimeoutManager.cs | 134 ++++++++++++++++++++++++++++++++++++ NHttp/NHttp.csproj | 1 + 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 NHttp/HttpTimeoutManager.cs diff --git a/NHttp/HttpClient.cs b/NHttp/HttpClient.cs index 83cb032..03e46ac 100644 --- a/NHttp/HttpClient.cs +++ b/NHttp/HttpClient.cs @@ -118,7 +118,10 @@ private void BeginRead() try { - ReadBuffer.BeginRead(_stream, ReadCallback, null); + Server.TimeoutManager.ReadQueue.Add( + ReadBuffer.BeginRead(_stream, ReadCallback, null), + this + ); } catch (Exception ex) { @@ -154,6 +157,12 @@ private void ReadCallback(IAsyncResult asyncResult) else Dispose(); } + catch (ObjectDisposedException ex) + { + Log.Info("Failed to read", ex); + + Dispose(); + } catch (Exception ex) { Log.Info("Failed to read from the HTTP connection", ex); @@ -395,7 +404,10 @@ private void BeginWrite() int read = _writeStream.Read(_writeBuffer, 0, _writeBuffer.Length); - _stream.BeginWrite(_writeBuffer, 0, read, WriteCallback, null); + Server.TimeoutManager.WriteQueue.Add( + _stream.BeginWrite(_writeBuffer, 0, read, WriteCallback, null), + this + ); } catch (Exception ex) { @@ -619,6 +631,9 @@ public void UnsetParser() private void ProcessException(Exception exception) { + if (_disposed) + return; + _errored = true; // If there is no request available, the error didn't occur as part diff --git a/NHttp/HttpReadBuffer.cs b/NHttp/HttpReadBuffer.cs index 41352ee..66964e9 100644 --- a/NHttp/HttpReadBuffer.cs +++ b/NHttp/HttpReadBuffer.cs @@ -163,7 +163,7 @@ public bool CopyToStream(Stream stream, int maximum, byte[] boundary) return true; } - public void BeginRead(Stream stream, AsyncCallback callback, object state) + public IAsyncResult BeginRead(Stream stream, AsyncCallback callback, object state) { // A new read was requested. Reset the flag. @@ -215,7 +215,7 @@ public void BeginRead(Stream stream, AsyncCallback callback, object state) int bufferAvailable = Math.Min(_buffer.Length - _available, _bufferSize); - stream.BeginRead(_buffer, _available, bufferAvailable, callback, state); + return stream.BeginRead(_buffer, _available, bufferAvailable, callback, state); } public void EndRead(Stream stream, IAsyncResult asyncResult) diff --git a/NHttp/HttpServer.cs b/NHttp/HttpServer.cs index a99aea6..b9fdd7b 100644 --- a/NHttp/HttpServer.cs +++ b/NHttp/HttpServer.cs @@ -71,10 +71,16 @@ protected virtual void OnStateChanged(EventArgs e) public string ServerBanner { get; set; } + public TimeSpan ReadTimeout { get; set; } + + public TimeSpan WriteTimeout { get; set; } + public TimeSpan ShutdownTimeout { get; set; } internal HttpServerUtility ServerUtility { get; private set; } + internal HttpTimeoutManager TimeoutManager { get; private set; } + public HttpServer() { EndPoint = new IPEndPoint(IPAddress.Loopback, 0); @@ -82,6 +88,8 @@ public HttpServer() ReadBufferSize = 4096; WriteBufferSize = 4096; ShutdownTimeout = TimeSpan.FromSeconds(30); + ReadTimeout = TimeSpan.FromSeconds(90); + WriteTimeout = TimeSpan.FromSeconds(90); ServerBanner = String.Format("NHttp/{0}", GetType().Assembly.GetName().Version); } @@ -94,6 +102,8 @@ public void Start() Log.Debug(String.Format("Starting HTTP server at {0}", EndPoint)); + TimeoutManager = new HttpTimeoutManager(this); + // Start the listener. var listener = new TcpListener(EndPoint); @@ -322,6 +332,12 @@ public void Dispose() _clientsChangedEvent = null; } + if (TimeoutManager != null) + { + TimeoutManager.Dispose(); + TimeoutManager = null; + } + _disposed = true; } } diff --git a/NHttp/HttpTimeoutManager.cs b/NHttp/HttpTimeoutManager.cs new file mode 100644 index 0000000..4ec227c --- /dev/null +++ b/NHttp/HttpTimeoutManager.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace NHttp +{ + internal class HttpTimeoutManager : IDisposable + { + private Thread _thread; + private ManualResetEvent _closeEvent = new ManualResetEvent(false); + + public TimeoutQueue ReadQueue { get; private set; } + public TimeoutQueue WriteQueue { get; private set; } + + public HttpTimeoutManager(HttpServer server) + { + if (server == null) + throw new ArgumentNullException(nameof(server)); + + ReadQueue = new TimeoutQueue(server.ReadTimeout); + WriteQueue = new TimeoutQueue(server.WriteTimeout); + + _thread = new Thread(ThreadProc); + _thread.Start(); + } + + private void ThreadProc() + { + while (!_closeEvent.WaitOne(TimeSpan.FromSeconds(1))) + { + ProcessQueue(ReadQueue); + ProcessQueue(WriteQueue); + } + } + + private void ProcessQueue(TimeoutQueue queue) + { + while (true) + { + var item = queue.DequeueExpired(); + if (item == null) + return; + + if (!item.AsyncResult.IsCompleted) + { + try + { + item.Disposable.Dispose(); + } + catch + { + // Ignore exceptions. + } + } + } + } + + public void Dispose() + { + if (_thread != null) + { + _closeEvent.Set(); + _thread.Join(); + _thread = null; + } + if (_closeEvent != null) + { + _closeEvent.Close(); + _closeEvent = null; + } + } + + public class TimeoutQueue + { + private readonly object _syncRoot = new object(); + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private readonly long _timeout; + private readonly Queue _items = new Queue(); + + public TimeoutQueue(TimeSpan timeout) + { + _timeout = (long)(timeout.TotalSeconds * Stopwatch.Frequency); + } + + public void Add(IAsyncResult asyncResult, IDisposable disposable) + { + if (asyncResult == null) + throw new ArgumentNullException(nameof(asyncResult)); + if (disposable == null) + throw new ArgumentNullException(nameof(disposable)); + + lock (_syncRoot) + { + _items.Enqueue(new TimeoutItem(_stopwatch.ElapsedTicks + _timeout, asyncResult, disposable)); + } + } + + public TimeoutItem DequeueExpired() + { + lock (_syncRoot) + { + if (_items.Count == 0) + return null; + + var item = _items.Peek(); + if (item.Expires < _stopwatch.ElapsedTicks) + return _items.Dequeue(); + + return null; + } + } + } + + public class TimeoutItem + { + public long Expires { get; private set; } + public IAsyncResult AsyncResult { get; private set; } + public IDisposable Disposable { get; private set; } + + public TimeoutItem(long expires, IAsyncResult asyncResult, IDisposable disposable) + { + if (asyncResult == null) + throw new ArgumentNullException(nameof(asyncResult)); + + Expires = expires; + AsyncResult = asyncResult; + Disposable = disposable; + } + } + } +} diff --git a/NHttp/NHttp.csproj b/NHttp/NHttp.csproj index 2b03968..d3596ae 100644 --- a/NHttp/NHttp.csproj +++ b/NHttp/NHttp.csproj @@ -57,6 +57,7 @@ +