#region Copyright 2011-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Principal;
using System.Threading;
namespace CSharpTest.Net.Http
{
///
/// Hosts an HttpListener on a dedicated set of worker threads, providing a clean shutdown
/// on dispose.
///
public class HttpServer : IDisposable
{
[ThreadStatic]
private static HttpListenerContext _threadContext;
private readonly HttpListener _listener;
private readonly Thread _listenerThread;
private readonly Thread[] _workers;
private readonly ManualResetEvent _stop, _ready;
private Queue _queue;
///
/// Constructs the HttpServer with a fixed thread-pool size.
///
public HttpServer(int maxThreads)
{
_workers = new Thread[maxThreads];
_queue = new Queue();
_stop = new ManualResetEvent(false);
_ready = new ManualResetEvent(false);
_listener = new HttpListener();
_listenerThread = new Thread(HandleRequests);
}
///
/// Returns a thread-static HttpListenerContext of the current http request, or null if there is none.
///
public static HttpListenerContext Context {
get { return _threadContext; }
}
///
/// Exposes a WaitHandle that can be used to signal other threads that the HTTP server is shutting down.
///
public WaitHandle ShutdownEvent {
get { return _stop; }
}
///
/// Returns the path being used to host this server instance, for example return "/root/" when prefixes = "http://*:8080/root"
///
public string ApplicationPath { get; private set; }
///
/// Raised when an unhandled exception occurs, you can use HttpServer.Context to respond to the http client if needed.
///
public event EventHandler OnError;
///
/// Performs the processing of the request on one of the worker threads
///
public event EventHandler ProcessRequest;
///
/// Starts the HttpServer listening on the prefixes supplied. use "http://+:80/" to match any host identifier not already mapped,
/// or "http://*:80/" to match all host identifiers.
///
/// see http://msdn.microsoft.com/en-us/library/system.net.httplistenerprefixcollection.add.aspx
public void Start(string[] prefixes)
{
string baseUri = null;
foreach (var prefix in prefixes)
{
_listener.Prefixes.Add(prefix);
string tmp = prefix.Replace("://*", "://hostname").Replace("://+", "://hostname");
if (baseUri == null)
baseUri = new Uri(tmp).AbsolutePath;
else if (baseUri != new Uri(tmp).AbsolutePath)
throw new ArgumentException("Must use the same path for all prefixes.", "prefixes");
}
ApplicationPath = baseUri;
try
{
_listener.Start();
}
catch (HttpListenerException ex)
{
if (ex.ErrorCode == 5)
{
StringWriter swMsg = new StringWriter();
swMsg.WriteLine(ex.Message);
WindowsIdentity user = WindowsIdentity.GetCurrent();
swMsg.WriteLine("Use the following command(s) to grant access:");
foreach (var prefix in prefixes)
{
swMsg.WriteLine(" netsh http add urlacl url={0} \"user={1}\" listen=yes",
prefix, user == null ? "NT AUTHORITY\\Everyone" : user.Name);
}
throw new UnauthorizedAccessException(swMsg.ToString(), ex);
}
throw;
}
_listenerThread.Start();
for (int i = 0; i < _workers.Length; i++)
{
_workers[i] = new Thread(Worker);
_workers[i].Start();
}
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{ Stop(); }
///
/// Stops the HTTP server and all worker threads.
///
public void Stop()
{
_stop.Set();
try { _listenerThread.Join(); }
catch { }
foreach (Thread worker in _workers)
{
try { worker.Join(); }
catch { }
}
try { _listener.Stop(); }
catch { }
}
private void HandleRequests()
{
try
{
while (_listener.IsListening)
{
var context = _listener.BeginGetContext(ContextReady, null);
if (0 == WaitHandle.WaitAny(new[] {_stop, context.AsyncWaitHandle}))
return;
}
}
catch { _stop.Set(); }
}
private void ContextReady(IAsyncResult ar)
{
try
{
lock (_queue)
{
_queue.Enqueue(_listener.EndGetContext(ar));
_ready.Set();
}
}
catch { return; }
}
private void Worker()
{
WaitHandle[] wait = new[] { _ready, _stop };
while (0 == WaitHandle.WaitAny(wait))
{
HttpListenerContext context;
lock (_queue)
{
if (_queue.Count > 0)
context = _queue.Dequeue();
else
{
_ready.Reset();
continue;
}
}
try
{
_threadContext = context;
ProcessRequest(this, new HttpContextEventArgs(this, context));
}
catch (Exception ex)
{
EventHandler e = OnError;
if (e != null)
try { e(context, new ErrorEventArgs(ex)); } catch { }
}
finally
{
_threadContext = null;
}
}
}
}
///
/// The event type used for the processing event to access the HttpListenerContext and the
/// HttpServer instance.
///
public sealed class HttpContextEventArgs : EventArgs
{
internal HttpContextEventArgs(HttpServer host, HttpListenerContext context)
{
Host = host;
Context = context;
}
/// The HttpServer recieving the request
public HttpServer Host { get; private set; }
/// The HttpListenerContext for this request
public HttpListenerContext Context { get; private set; }
}
}