#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Net.Sockets; using System.Threading; using Nuclex.Support.Collections; namespace Nuclex.Networking.Http { /// Simple HTTP server that answers requests by clients public class HttpServer : IDisposable { #region class ConnectionEntry /// Encapsulates an HTTP client connection managed by the server private class ConnectionEntry { /// Initializes a new connection entry /// Connected HTTP client to store in the entry public ConnectionEntry(ClientConnection connection) { this.Connection = connection; } /// Connected HTTP client managed by this entry public volatile ClientConnection Connection; } #endregion // class ConnectionEntry /// Initializes a new HTTP server on port 80 public HttpServer() : this(80) { } /// Initializes a new HTTP server /// /// TCP port the server should liston on for incoming connections /// public HttpServer(short port) { this.port = port; this.clientConnectedDelegate = new EventHandler(clientConnected); this.connectedClients = new Dictionary(); this.clientCleanupQueue = new PairPriorityQueue( new ReverseComparer() ); this.idleConnectionDropTime = new TimeSpan(0, 0, 30); this.idleConnectionCleanupThreadWakeupEvent = new ManualResetEvent(false); } /// Starts the http server public void Start() { lock(this) { // If the listener does not exist that indicates that we're not running. Do nothing // otherwise (we allow multiple Start()s - the redundant calls will simply do nothing) if(this.listener == null) { // Start the idle connection cleanup thread. We use an explicit thread here // instead of a thread pool wait request in order to guarantee connection cleanup. // If the server becomes very loaded (to the point of running out of thread pool // threads), reliability would otherwise suffer because the idle connection // cleaner might in itself be queued in the thread pool amongst all the connection // processing tasks. // // This is not a protection against a DoS attack since it's still possible to // flood the server with countless connections. Flooding still has to be detected // and resolved by the implementer if so desired. this.stopIdleConnectionCleanupThread = false; this.idleConnectionCleanupThread = new Thread( new ThreadStart(runIdleConnectionCleanupLoop) ); this.idleConnectionCleanupThread.Name = "HTTP Server Idle Connection Cleaner"; this.idleConnectionCleanupThread.IsBackground = true; this.idleConnectionCleanupThread.Start(); // Start the socket listener this.listener = new SocketListener(this.port); this.listener.ClientConnected += this.clientConnectedDelegate; this.listener.StartListening(); } } } /// Stops the http server public void Stop() { lock(this) { // The existence of the listener indicates that we're still running. Do nothing // otherwise (we allow multiple Stop()s - the redundant calls will simply do nothing) if(this.listener != null) { // Stop the listener first to ensure we will receive no more connections this.listener.ClientConnected -= this.clientConnectedDelegate; this.listener.Dispose(); this.listener = null; // Now stop the idle connection cleanup thread - we're going to drop all connections // and don't want that thread messing things up for us this.stopIdleConnectionCleanupThread = true; this.idleConnectionCleanupThreadWakeupEvent.Set(); this.idleConnectionCleanupThread.Join(); this.idleConnectionCleanupThread = null; // No more incoming connections will be accepted and the idle connection cleanup // thread is stopped, now we can safely drop all client connections DropAllClients(); } } } /// Drops all clients currently connected to the server public void DropAllClients() { int connectionCount; ClientConnection[] clientConnections; lock(this.connectedClients) { connectionCount = this.connectedClients.Count; clientConnections = new ClientConnection[connectionCount]; this.connectedClients.Keys.CopyTo(clientConnections, 0); } for(; connectionCount > 0; --connectionCount) { clientConnections[connectionCount - 1].Drop(); } } /// Immediately releases all resources used by the instance public void Dispose() { Stop(); } /// Time after which the server will drop inactive connections /// /// This is mainly a safeguard against faulty clients. If a client doesn't close /// his connection after a reasonable idle time (internet standards suggest /// 15-30 seconds), the server will take action. Connectivity breakdowns in /// clients can also result in connections dying without proper termination and /// would result in the server slowly building up more and more connections. /// public TimeSpan IdleConnectionDropTime { get { return this.idleConnectionDropTime; } set { lock(this) { this.idleConnectionDropTime = value; this.idleConnectionCleanupThreadWakeupEvent.Set(); } } } /// Called to accepts incoming client connections /// Socket of the connected client /// /// A new client connection responsible for managing requests by the /// connected client. /// protected virtual ClientConnection AcceptClientConnection(Socket connectedSocket) { return new ClientConnection(this, connectedSocket); } /// Notifies the server that a client has disconnected /// Client that has disconnected internal void NotifyClientDisconnected(ClientConnection client) { lock(this.connectedClients) { ConnectionEntry entry; if(this.connectedClients.TryGetValue(client, out entry)) { ClientConnection connection = entry.Connection; entry.Connection = null; this.connectedClients.Remove(connection); } } } /// Cleans up idle connections after they have timed out private void runIdleConnectionCleanupLoop() { for(; ; ) { DateTime cleanupCycleStartTime = DateTime.UtcNow; TimeSpan timeToNextCleanup; // Clean up all connections that have been idling for longer than they // are allowed to for(; ; ) { // If the HTTP server wants us to stop, break the loop here! if(this.stopIdleConnectionCleanupThread) { return; } // Take out the next item that needs to be cleaned. We take the lock only for // this moment to avoid stalling the acceptance of new connections while we're // cleaning up this connection. PriorityItemPair nextConnectionToClean; lock(this.clientCleanupQueue) { if(this.clientCleanupQueue.Count == 0) { timeToNextCleanup = this.idleConnectionDropTime; break; } nextConnectionToClean = this.clientCleanupQueue.Peek(); } // See if this connection is a candidate for cleanup TimeSpan idleTime = cleanupCycleStartTime - nextConnectionToClean.Priority; if(idleTime > this.idleConnectionDropTime) { // The connection was idle for too long - clean it up. The call to Drop() // will call back on us to take the connection out of the normal connection // list, we only need to bother with our idle connection cleanup queue. lock(this.clientCleanupQueue) { this.clientCleanupQueue.Dequeue(); ClientConnection connection = nextConnectionToClean.Item.Connection; if(!ReferenceEquals(connection, null)) { connection.Drop(); } } } else { // The connection has not yet reached the idle timeout. Since we're using a // priority queue, we can be sure that this is the connection that was idle // the longest and that there will be no other timeouts occuring before it. idleTime = DateTime.UtcNow - nextConnectionToClean.Priority; timeToNextCleanup = idleTime + OneSecondTimeSpan; break; } } // for(;;) // Wait until the next connection becomes idle or we're waken up this.idleConnectionCleanupThreadWakeupEvent.WaitOne(timeToNextCleanup, false); } // for(;;) } /// Called when a client connects to the http server /// The socket listener reporting the new connection /// Contains the socket of the connecting client private void clientConnected(object sender, SocketEventArgs arguments) { ClientConnection clientConnection; // An exception from the AcceptClientConnection method would end up in the // ThreadPool. In .NET 2.0, the behavior in this case is clearly defined // (ThreadPool prints exception to console and ignores it), but since this // would leave the client running into the timeout, we try to at least // shut down the socket gracefully and to pester the developer try { clientConnection = AcceptClientConnection(arguments.Socket); } catch(Exception exception) { System.Diagnostics.Trace.WriteLine( "AcceptClientConnection() threw an exception, dropping connection" ); System.Diagnostics.Trace.WriteLine( "Exception from AcceptClientConnection(): " + exception.ToString() ); System.Diagnostics.Trace.Assert( false, "AcceptClientConnection() threw an exception, connection will be dropped" ); // Send 500 internal server error here! try { arguments.Socket.Shutdown(SocketShutdown.Both); } finally { arguments.Socket.Close(); } throw; } try { ConnectionEntry entry = new ConnectionEntry(clientConnection); lock(this.clientCleanupQueue) { this.clientCleanupQueue.Enqueue(DateTime.UtcNow, entry); } lock(this.connectedClients) { this.connectedClients.Add(clientConnection, entry); } } catch(Exception) { clientConnection.Drop(); throw; } } /// Timespan with a length of 1 second private static readonly TimeSpan OneSecondTimeSpan = new TimeSpan(0, 0, 1); /// Listener used to accept incoming connections private SocketListener listener; /// Port the http server is listening on private short port; /// Delegate for the clientConnectd() method private EventHandler clientConnectedDelegate; /// clients sorted by time of their last transmission /// /// This queue will also retain entries for connections that have been closed due /// to means other than a timeout. In this case, the additional indirection provided /// by the ConnectionEntry will allow the queue item's connection to be set to /// null and thus, be silently discarded when its lifetime expires. /// private PairPriorityQueue clientCleanupQueue; /// Time until an idle connection will be dropped by the server private TimeSpan idleConnectionDropTime; /// Set to true to stop the idle connection cleanup thread private volatile bool stopIdleConnectionCleanupThread; /// Thread used to clean up idle connections from the server private Thread idleConnectionCleanupThread; /// Wakeup event for the idle connection cleanup thread private ManualResetEvent idleConnectionCleanupThreadWakeupEvent; /// Dictionary containing the clients currently connected to the server private Dictionary connectedClients; } } // namespace Nuclex.Networking.Http