create http listener abstraction

This commit is contained in:
Luke Pulverenti 2014-07-18 18:14:59 -04:00
parent b4b17d7717
commit ea559a6e27
9 changed files with 367 additions and 214 deletions

View File

@ -26,8 +26,7 @@ namespace MediaBrowser.Common.Net
/// Starts this instance.
/// </summary>
/// <param name="urlPrefixes">The URL prefixes.</param>
/// <param name="enableHttpLogging">if set to <c>true</c> [enable HTTP logging].</param>
void Start(IEnumerable<string> urlPrefixes, bool enableHttpLogging);
void Start(IEnumerable<string> urlPrefixes);
/// <summary>
/// Starts the web socket server.

View File

@ -38,12 +38,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
void Stop();
/// <summary>
/// Gets or sets a value indicating whether [enable HTTP request logging].
/// </summary>
/// <value><c>true</c> if [enable HTTP request logging]; otherwise, <c>false</c>.</value>
bool EnableHttpRequestLogging { get; set; }
/// <summary>
/// Occurs when [web socket connected].
/// </summary>

View File

@ -22,12 +22,6 @@ namespace MediaBrowser.Model.Configuration
/// <value>The weather unit.</value>
public WeatherUnits WeatherUnit { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [enable HTTP level logging].
/// </summary>
/// <value><c>true</c> if [enable HTTP level logging]; otherwise, <c>false</c>.</value>
public bool EnableHttpLevelLogging { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [enable u pn p].
/// </summary>
@ -223,7 +217,6 @@ namespace MediaBrowser.Model.Configuration
ImageSavingConvention = ImageSavingConvention.Compatible;
HttpServerPortNumber = 8096;
LegacyWebSocketPortNumber = 8945;
EnableHttpLevelLogging = true;
EnableDashboardResponseCaching = true;
EnableAutomaticRestart = true;

View File

@ -5,6 +5,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Logging;
using MediaBrowser.Server.Implementations.HttpServer.NetListener;
using ServiceStack;
using ServiceStack.Api.Swagger;
using ServiceStack.Host;
@ -34,14 +35,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer
private readonly List<IRestfulService> _restServices = new List<IRestfulService>();
private HttpListener Listener { get; set; }
protected bool IsStarted = false;
private IHttpListener _listener;
private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false);
private readonly SmartThreadPool _threadPoolManager;
private const int IdleTimeout = 300;
private const int IdleTimeout = 300;
private readonly ContainerAdapter _containerAdapter;
private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@ -53,7 +51,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <value>The local end points.</value>
public IEnumerable<string> LocalEndPoints
{
get { return _localEndPoints.Keys.ToList(); }
get { return _listener == null ? new List<string>() : _listener.LocalEndPoints; }
}
public HttpListenerHost(IApplicationHost applicationHost, ILogManager logManager, string serviceName, string handlerPath, string defaultRedirectPath, params Assembly[] assembliesWithServices)
@ -148,120 +146,32 @@ namespace MediaBrowser.Server.Implementations.HttpServer
public override ServiceStackHost Start(string listeningAtUrlBase)
{
StartListener(Listen);
StartListener();
return this;
}
/// <summary>
/// Starts the Web Service
/// </summary>
private void StartListener(WaitCallback listenCallback)
private void StartListener()
{
// *** Already running - just leave it in place
if (IsStarted)
return;
if (Listener == null)
Listener = new HttpListener();
HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
foreach (var prefix in UrlPrefixes)
_listener = new HttpListenerServer(_logger, _threadPoolManager)
{
_logger.Info("Adding HttpListener prefix " + prefix);
Listener.Prefixes.Add(prefix);
}
WebSocketHandler = WebSocketHandler,
ErrorHandler = ErrorHandler,
RequestHandler = RequestHandler
};
IsStarted = true;
_logger.Info("Starting HttpListner");
Listener.Start();
_logger.Info("HttpListener started");
ThreadPool.QueueUserWorkItem(listenCallback);
_listener.Start(UrlPrefixes);
}
private bool IsListening
private void WebSocketHandler(WebSocketConnectEventArgs args)
{
get { return this.IsStarted && this.Listener != null && this.Listener.IsListening; }
}
// Loop here to begin processing of new requests.
private void Listen(object state)
{
while (IsListening)
if (WebSocketConnected != null)
{
if (Listener == null) return;
try
{
Listener.BeginGetContext(ListenerCallback, Listener);
_listenForNextRequest.WaitOne();
}
catch (Exception ex)
{
_logger.Error("Listen()", ex);
return;
}
if (Listener == null) return;
}
}
// Handle the processing of a request in here.
private void ListenerCallback(IAsyncResult asyncResult)
{
var listener = asyncResult.AsyncState as HttpListener;
HttpListenerContext context;
if (listener == null) return;
var isListening = listener.IsListening;
try
{
if (!isListening)
{
_logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
}
// The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
// blocks until there is a request to be processed or some type of data is available.
context = listener.EndGetContext(asyncResult);
}
catch (Exception ex)
{
// You will get an exception when httpListener.Stop() is called
// because there will be a thread stopped waiting on the .EndGetContext()
// method, and again, that is just the way most Begin/End asynchronous
// methods of the .NET Framework work.
var errMsg = ex + ": " + IsListening;
_logger.Warn(errMsg);
return;
}
finally
{
// Once we know we have a request (or exception), we signal the other thread
// so that it calls the BeginGetContext() (or possibly exits if we're not
// listening any more) method to start handling the next incoming request
// while we continue to process this request on a different thread.
_listenForNextRequest.Set();
}
_threadPoolManager.QueueWorkItem(() => InitTask(context));
}
public virtual void InitTask(HttpListenerContext context)
{
try
{
var task = this.ProcessRequestAsync(context);
task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
if (task.Status == TaskStatus.Created)
{
task.RunSynchronously();
}
}
catch (Exception ex)
{
HandleError(ex, context);
WebSocketConnected(this, args);
}
}
@ -280,43 +190,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
_localEndPoints.GetOrAdd(address, address);
}
if (EnableHttpRequestLogging)
{
LoggerUtils.LogRequest(_logger, request);
}
LoggerUtils.LogRequest(_logger, request);
}
/// <summary>
/// Processes the web socket request.
/// </summary>
/// <param name="ctx">The CTX.</param>
/// <returns>Task.</returns>
private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
{
#if !__MonoCS__
try
{
var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (WebSocketConnected != null)
{
WebSocketConnected(this, new WebSocketConnectEventArgs { WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() });
}
}
catch (Exception ex)
{
_logger.ErrorException("AcceptWebSocketAsync error", ex);
ctx.Response.StatusCode = 500;
ctx.Response.Close();
}
#endif
}
private void HandleError(Exception ex, HttpListenerContext context)
private void ErrorHandler(Exception ex, IRequest httpReq)
{
try
{
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
var httpRes = httpReq.Response;
if (httpRes.IsClosed)
@ -366,84 +246,54 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
}
private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
{
var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
req.RequestAttributes = req.GetAttributes();
return req;
}
/// <summary>
/// Shut down the Web Service
/// </summary>
public void Stop()
{
if (Listener != null)
if (_listener != null)
{
foreach (var prefix in UrlPrefixes)
{
Listener.Prefixes.Remove(prefix);
}
Listener.Close();
_listener.Stop();
}
}
/// <summary>
/// Overridable method that can be used to implement a custom hnandler
/// </summary>
/// <param name="context"></param>
protected Task ProcessRequestAsync(HttpListenerContext context)
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Task.</returns>
protected Task RequestHandler(IHttpRequest httpReq, Uri url)
{
var request = context.Request;
var date = DateTime.Now;
LogHttpRequest(request);
var httpRes = httpReq.Response;
if (request.IsWebSocketRequest)
{
return ProcessWebSocketRequest(context);
}
var localPath = request.Url.LocalPath;
var operationName = httpReq.OperationName;
var localPath = url.LocalPath;
if (string.Equals(localPath, "/" + HandlerPath + "/", StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(DefaultRedirectPath);
context.Response.Close();
httpRes.RedirectToUrl(DefaultRedirectPath);
return Task.FromResult(true);
}
if (string.Equals(localPath, "/" + HandlerPath, StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath);
return Task.FromResult(true);
}
if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath);
return Task.FromResult(true);
}
if (string.IsNullOrEmpty(localPath))
{
context.Response.Redirect("/" + HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
httpRes.RedirectToUrl("/" + HandlerPath + "/" + DefaultRedirectPath);
return Task.FromResult(true);
}
var date = DateTime.Now;
if (string.IsNullOrEmpty(context.Request.RawUrl))
return ((object)null).AsTaskResult();
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
var httpRes = httpReq.Response;
var handler = HttpHandlerFactory.GetHandler(httpReq);
var url = request.Url.ToString();
var remoteIp = httpReq.RemoteIp;
var serviceStackHandler = handler as IServiceStackHandler;
@ -461,15 +311,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
//Matches Exceptions handled in HttpListenerBase.InitTask()
var statusCode = httpRes.StatusCode;
var urlString = url.ToString();
task.ContinueWith(x =>
{
var duration = DateTime.Now - date;
if (EnableHttpRequestLogging)
{
LoggerUtils.LogResponse(_logger, statusCode, url, remoteIp, duration);
}
LoggerUtils.LogResponse(_logger, statusCode, urlString, remoteIp, duration);
}, TaskContinuationOptions.None);
@ -480,12 +328,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
.AsTaskException();
}
/// <summary>
/// Gets or sets a value indicating whether [enable HTTP request logging].
/// </summary>
/// <value><c>true</c> if [enable HTTP request logging]; otherwise, <c>false</c>.</value>
public bool EnableHttpRequestLogging { get; set; }
/// <summary>
/// Adds the rest handlers.
/// </summary>
@ -530,8 +372,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
if (disposing)
{
_threadPoolManager.Dispose();
_threadPoolManager.Dispose();
Stop();
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using ServiceStack.Web;
using System;
using System.Collections.Generic;
namespace MediaBrowser.Server.Implementations.HttpServer
{
public interface IHttpListener : IDisposable
{
IEnumerable<string> LocalEndPoints { get; }
/// <summary>
/// Gets or sets the error handler.
/// </summary>
/// <value>The error handler.</value>
Action<Exception, IRequest> ErrorHandler { get; set; }
/// <summary>
/// Gets or sets the request handler.
/// </summary>
/// <value>The request handler.</value>
Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
/// <summary>
/// Gets or sets the web socket handler.
/// </summary>
/// <value>The web socket handler.</value>
Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
/// <summary>
/// Starts this instance.
/// </summary>
/// <param name="urlPrefixes">The URL prefixes.</param>
void Start(IEnumerable<string> urlPrefixes);
/// <summary>
/// Stops this instance.
/// </summary>
void Stop();
}
}

View File

@ -0,0 +1,281 @@
using Amib.Threading;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Logging;
using ServiceStack;
using ServiceStack.Host.HttpListener;
using ServiceStack.Web;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.HttpServer.NetListener
{
public class HttpListenerServer : IHttpListener
{
private readonly ILogger _logger;
private HttpListener _listener;
private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false);
private readonly SmartThreadPool _threadPoolManager;
public System.Action<Exception, IRequest> ErrorHandler { get; set; }
public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
public System.Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public HttpListenerServer(ILogger logger, SmartThreadPool threadPoolManager)
{
_logger = logger;
_threadPoolManager = threadPoolManager;
}
/// <summary>
/// Gets the local end points.
/// </summary>
/// <value>The local end points.</value>
public IEnumerable<string> LocalEndPoints
{
get { return _localEndPoints.Keys.ToList(); }
}
private List<string> UrlPrefixes { get; set; }
public void Start(IEnumerable<string> urlPrefixes)
{
UrlPrefixes = urlPrefixes.ToList();
if (_listener == null)
_listener = new System.Net.HttpListener();
//HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
foreach (var prefix in UrlPrefixes)
{
_logger.Info("Adding HttpListener prefix " + prefix);
_listener.Prefixes.Add(prefix);
}
_listener.Start();
ThreadPool.QueueUserWorkItem(Listen);
}
private bool IsListening
{
get { return _listener != null && _listener.IsListening; }
}
// Loop here to begin processing of new requests.
private void Listen(object state)
{
while (IsListening)
{
if (_listener == null) return;
try
{
_listener.BeginGetContext(ListenerCallback, _listener);
_listenForNextRequest.WaitOne();
}
catch (Exception ex)
{
_logger.Error("Listen()", ex);
return;
}
if (_listener == null) return;
}
}
// Handle the processing of a request in here.
private void ListenerCallback(IAsyncResult asyncResult)
{
var listener = asyncResult.AsyncState as HttpListener;
HttpListenerContext context;
if (listener == null) return;
var isListening = listener.IsListening;
try
{
if (!isListening)
{
_logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
}
// The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
// blocks until there is a request to be processed or some type of data is available.
context = listener.EndGetContext(asyncResult);
}
catch (Exception ex)
{
// You will get an exception when httpListener.Stop() is called
// because there will be a thread stopped waiting on the .EndGetContext()
// method, and again, that is just the way most Begin/End asynchronous
// methods of the .NET Framework work.
var errMsg = ex + ": " + IsListening;
_logger.Warn(errMsg);
return;
}
finally
{
// Once we know we have a request (or exception), we signal the other thread
// so that it calls the BeginGetContext() (or possibly exits if we're not
// listening any more) method to start handling the next incoming request
// while we continue to process this request on a different thread.
_listenForNextRequest.Set();
}
_threadPoolManager.QueueWorkItem(() => InitTask(context));
}
public virtual void InitTask(HttpListenerContext context)
{
try
{
var task = this.ProcessRequestAsync(context);
task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
if (task.Status == TaskStatus.Created)
{
task.RunSynchronously();
}
}
catch (Exception ex)
{
HandleError(ex, context);
}
}
protected Task ProcessRequestAsync(HttpListenerContext context)
{
var request = context.Request;
LogHttpRequest(request);
if (request.IsWebSocketRequest)
{
return ProcessWebSocketRequest(context);
}
if (string.IsNullOrEmpty(context.Request.RawUrl))
return ((object)null).AsTaskResult();
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
return RequestHandler(httpReq, request.Url);
}
/// <summary>
/// Processes the web socket request.
/// </summary>
/// <param name="ctx">The CTX.</param>
/// <returns>Task.</returns>
private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
{
#if !__MonoCS__
try
{
var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (WebSocketHandler != null)
{
WebSocketHandler(new WebSocketConnectEventArgs
{
WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger),
Endpoint = ctx.Request.RemoteEndPoint.ToString()
});
}
}
catch (Exception ex)
{
_logger.ErrorException("AcceptWebSocketAsync error", ex);
ctx.Response.StatusCode = 500;
ctx.Response.Close();
}
#endif
}
private void HandleError(Exception ex, HttpListenerContext context)
{
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
if (ErrorHandler != null)
{
ErrorHandler(ex, httpReq);
}
}
private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
{
var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
req.RequestAttributes = req.GetAttributes();
return req;
}
/// <summary>
/// Logs the HTTP request.
/// </summary>
/// <param name="request">The request.</param>
private void LogHttpRequest(HttpListenerRequest request)
{
var endpoint = request.LocalEndPoint;
if (endpoint != null)
{
var address = endpoint.ToString();
_localEndPoints.GetOrAdd(address, address);
}
LoggerUtils.LogRequest(_logger, request);
}
public void Stop()
{
if (_listener != null)
{
foreach (var prefix in UrlPrefixes)
{
_listener.Prefixes.Remove(prefix);
}
_listener.Close();
}
}
public void Dispose()
{
Dispose(true);
}
private bool _disposed;
private readonly object _disposeLock = new object();
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
lock (_disposeLock)
{
if (_disposed) return;
if (disposing)
{
_threadPoolManager.Dispose();
Stop();
}
//release unmanaged resources here...
_disposed = true;
}
}
}
}

View File

@ -96,6 +96,9 @@
<HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath>
</Reference>
<Reference Include="Mono.Posix" Condition=" '$(ConfigurationName)' == 'Release Mono' " />
<Reference Include="websocket-sharp">
<HintPath>..\ThirdParty\WebsocketSharp\websocket-sharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs">
@ -134,6 +137,8 @@
<Compile Include="EntryPoints\ServerEventNotifier.cs" />
<Compile Include="EntryPoints\UserDataChangeNotifier.cs" />
<Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
<Compile Include="HttpServer\NetListener\HttpListenerServer.cs" />
<Compile Include="HttpServer\IHttpListener.cs" />
<Compile Include="HttpServer\Security\AuthorizationContext.cs" />
<Compile Include="HttpServer\ContainerAdapter.cs" />
<Compile Include="HttpServer\GetSwaggerResource.cs" />

View File

@ -123,9 +123,9 @@ namespace MediaBrowser.Server.Implementations.ServerManager
/// <summary>
/// Starts this instance.
/// </summary>
public void Start(IEnumerable<string> urlPrefixes, bool enableHttpLogging)
public void Start(IEnumerable<string> urlPrefixes)
{
ReloadHttpServer(urlPrefixes, enableHttpLogging);
ReloadHttpServer(urlPrefixes);
}
public void StartWebSocketServer()
@ -152,14 +152,13 @@ namespace MediaBrowser.Server.Implementations.ServerManager
/// <summary>
/// Restarts the Http Server, or starts it if not currently running
/// </summary>
private void ReloadHttpServer(IEnumerable<string> urlPrefixes, bool enableHttpLogging)
private void ReloadHttpServer(IEnumerable<string> urlPrefixes)
{
_logger.Info("Loading Http Server");
try
{
HttpServer = _applicationHost.Resolve<IHttpServer>();
HttpServer.EnableHttpRequestLogging = enableHttpLogging;
HttpServer.StartServer(urlPrefixes);
}
catch (SocketException ex)

View File

@ -851,7 +851,7 @@ namespace MediaBrowser.ServerApplication
{
try
{
ServerManager.Start(HttpServerUrlPrefixes, ServerConfigurationManager.Configuration.EnableHttpLevelLogging);
ServerManager.Start(HttpServerUrlPrefixes);
}
catch (Exception ex)
{
@ -881,8 +881,6 @@ namespace MediaBrowser.ServerApplication
{
base.OnConfigurationUpdated(sender, e);
HttpServer.EnableHttpRequestLogging = ServerConfigurationManager.Configuration.EnableHttpLevelLogging;
if (!HttpServer.UrlPrefixes.SequenceEqual(HttpServerUrlPrefixes, StringComparer.OrdinalIgnoreCase))
{
NotifyPendingRestart();