Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Small and Reliable C++ HTTP Server with Complete ASP.NET Support

4.78/5 (24 votes)
5 Feb 2009CPOL8 min read 76.2K   1.6K  
This article describes results of ahttpserver evolution - implementation of ASP.NET handler and many architecture improvements

Introduction

This article describes changes in ahttp library made from first release. I have decided to upload a new article because the server code was greatly modified and at present it is almost a production ready project.

The current server version can be easily used as an IIS replacement in ASP.NET applications development. Server supported wildcard ('.*') mapping with exclude exception list maintaining - this feature can be used to setup ASP.NET MVC application (see example in attached demo).

After first ahttp version sharing (about year ago), I have continued working on this project. The main goal of this development stayed unchanged - investigate and use latest features available in C++, apply different known design practices to create stable and extendable application architecture. This project is developed in my free time and is not related to my direct work - in office life mostly I work on web-applications (ASP.NET, JavaScript, jQuery, last time I participated in a project based on ASP.MVC).

Currently, the ahttp project contains three main parts:

  • aconnect static library: contains multithreaded TCP server implementation, file logger, and a great amount of utility functionality - TCP socket control, string processing algorithms, date/time functions, cryptography
  • ahttplib static library: HttpServer definition (see details in first article) and all HTTP requests parsing/processing functionality, server settings loading code
  • ahttpserver: simple HTTP server application

The server still supports Windows and Linux platforms (tested on Ubuntu only).

To extend server features, a set of plugins was developed:

  • handler_aspnet - ASP.NET application support (available only under Windows). Common architecture of this plugin was copied from .NET Cassini server
  • handler_isapi - IIS ISAPI extensions wrapper - using this wrapper ahttpserver can utilize already developed extensions to support different script engines. This handler works correctly with PHP 4 and 5, I have tried to use ASP and ASP.NET ISAPI extensions but both of them use undocumented features of IIS and cannot be loaded into ahttpserver
  • handler_python - Python scripts support, works differently from general approach to server-side Python - this module executes scripts directly
  • module_authbasic - Basic authentication support - two types of auth. providers available. Server provider authenticates users against list loaded from file and system provider (now works only under Windows) authenticates users against OS.

Using the Code

Server/Library Architecture

All HTTP server kernel code is located in the ahttp library to make possible embedding of the server into any existing architecture or use only necessary parts of server code, like HTTP requests parsing. Using this library provides a developer the ability to create customizable web-server to process specific HTTP requests - SOAP requests, files downloading and so on.

Complete standalone HTTP server application (ahttpserver) included in project sources has only about 15 KB of sources (except libraries code of course). Thus using of this library can greatly decrease developing/prototyping efforts at server-side project estimation. Even if it will be decided not to use this library as service base, one can use the provided source parts to include in their own project.

To get ahttp::HttpServer working, one need have filled HttpServerSettings instance - even for simple server, there are many settings that can be setup. As a result, the preferred place to store these settings is an XML file which can be updated by hand and loaded quickly.

Server Settings File

XML
<?xml version="1.0" encoding="utf-8"?>
<settings>
    <server
	    version = "ahttp/0.17"
	    port="5555"
	    ip-address="0.0.0.0"
	    workers-count="50"
	    pooling-enabled="true"
	    worker-life-time="60"
	    command-port="5556"
	    root="root"

	    keep-alive-enabled = "true"
	    keep-alive-timeout = "10"
	    server-socket-timeout = "900"
	    command-socket-timeout = "30"

	    response-buffer-size = "8194304"
	    max-chunk-size = "512144"

	    directory-config-file = "directory.config"
	    messages-file = "messages.config"

	    uploads-dir = "c:\\temp\\ahttp"
	    locale=".1251"
	    >

	    <!-- log-level: "Debug", "Info", "Warning", "Error", "Critical" -
			if none of them - then debug -->
	    <log log-level="info" max-file-size="4194304">

		    <!-- {app-path} - path to directory where application
			is located (with trailing slash),
			     {timestamp} - generated timestamp -->
		    <path>{app-path}log\server_{timestamp}.log</path>
	    </log>

	    <mime-types file="{app-path}mime-types.config" />

	    <!-- All handlers must be registered there, concrete
		    assignments will be defined in <directory> elements -->
	    <handlers>
		    <register name="handler_python" default-ext=".py; .pyhtml">
			    <path>{app-path}handler_python-d.dll</path>
			    <!-- parameter name="uploads-dir">
				c:\temp\handler_python\</parameter -->
		    </register>
		    <register name="handler_php" default-ext=".php">
			    <path>{app-path}handler_isapi-d.dll</path>
			    <parameter name="engine">c:\PHP\php5isapi.dll</parameter>
			    <parameter name="update-path">c:\PHP\</parameter>
			    <parameter name="free-library">false</parameter>
			    <parameter name="check-file-exists">true</parameter>
		    </register>
		    <register name="handler_aspnet"
			default-ext=".aspx; .ashx; .asmx; .axd">
			    <path>{app-path}handler_aspnet-d.dll</path>
			    <parameter name="init-root">false</parameter>
			    <!-- parameter name="load-applications">mvc;
				books</parameter -->
		    </register>
	    </handlers>

        <!-- All modules must be registered there, concrete
        assignments will be defined in <directory> elements.
        'global' attribute defines that this module will be
	automatically applied to root directory.-->

        <modules>
            <register name="global_basic_auth" global="true">
	            <path>{app-path}module_authbasic-d.dll</path>
	            <parameter name="realm">Protected data</parameter>
	            <parameter name="provider">system</parameter>
	            <parameter name="default-domain">ES</parameter>
            </register>
        </modules>

    </server>

    <!-- virtual-path for root: "/"
			    'charset' - will be used when FS content is shown
			    default 'max-request-size': 2097152 bytes -->
    <directory name="root"
		    browsing-enabled="true"
		    charset="Windows-1251"
		    max-request-size="2097152"
		    enable-parent-path-access="true">

	    <path>d:\work\web\</path>

	    <default-documents>
		    <add>index.html</add>
		    <add>index.htm</add>
		    <add>main.html</add>
            <add>Default.aspx</add>
	    </default-documents>

	    <!-- ext="*" - will be applied to all requests -->
	    <!-- ext="." - will be applied to directory/file without extension -->
	    <handlers>
		    <add name="handler_python"/>
		    <add name="handler_php" />
		    <add name="handler_aspnet"/>
       </handlers>

	    <!-- Record attributes:
			    {name} - name of item,
			    {size} - size of item in kb,
			    {url} - url to open item
			    {time} - last modify dat/time of item,
			    {page-url} - url to current page
			    {parent-url} - url to parent directory
			    {files-count} - files count in current directory
			    {directories-count} - sub-directories count in
						current directory
			    {errors-count} - reading errors count
			    {tab} - will be replaced with '\t'
	    -->
	    	<header-template>
		<pre>{eol}
		<b>Directory: <i>{page-url}</i></b>{eol}{eol}
	</header-template>

	<parent-directory-template >
		<a href="{parent-url}">[parent directory]</a>{eol}{eol}
	</parent-directory-template>

	<directory-template>
		{time}{tab}{tab}directory{tab}{tab}<a href="{url}">{name}</a>{eol}
    </directory-template>

	<virtual-directory-template >
		{time}{tab}{tab}  virtual{tab}{tab}<a href="{url}">{name}</a>{eol}
    </virtual-directory-template>

	<file-template >
        {time}{tab}{size}{tab}{tab}<a href="{url}">{name}</a>{eol}
    </file-template>

	<footer-template>
        {eol}
		Files: {files-count}{eol}
		Directories: {directories-count}{eol}
		Reading errors: {errors-count}{eol}
		</pre>
	</footer-template>
    </directory>

    <directory name="server_data"
		    parent="root">
	    <virtual-path>server_data</virtual-path>
	    <path>{app-path}web</path>
    </directory>

    <directory name="mvc"
	       parent="root">
	    <handlers>
		    <add name="handler_aspnet" ext="*"/>
		    <remove name="handler_aspnet" ext=".gif; .js; .css; .jpg; .png"/>
	    </handlers>
	    <virtual-path>mvc</virtual-path>
	    <path>d:\work\Visual Studio 2008\Projects\
			OReilly-.NET3.5\MVCApplication\</path>
    </directory>
</settings>    

The first section of settings file - server defines HTTP server startup/runtime behavior: server's port (port attribute), IP address to bind on it (now only IPv4 is supported). Other parameters:

  • workers-count
    • Maximal worker threads count in thread's pool
  • pooling-enabled
    • Defines server working mode - single threaded (pooling-enabled = 'false') or multithreaded
  • worker-life-time
    • Worker thread release timeout in seconds
  • command-port
    • Used in ahttpserver to open additional listening port to receive server control commands ('start', 'stop', 'reload')
  • keep-alive-enabled
    • Setup HTTP Keep-Alive mode
  • server-socket-timeout
    • HTTP server socket read/write timeout (in seconds)
  • response-buffer-size
    • HTTP response buffer size (in bytes)
  • max-chunk-size
    • Maximal chunk size in chunked response mode (defined in bytes)
  • directory-config-file
    • Name of inplace file, located in server virtual directory and used to load default document list, plugins registration and server URL mappings setup (see sample in sources package)
  • messages-file
    • Server messages localization file
  • uploads-dir
    • Global uploads directory to store posted files content
  • locale
    • Important setting - call setlocale (LC_CTYPE, localeStr.c_str()) will be performed at server startup when this attribute is not empty. Locale setup can be used to force mbstowcs to work correctly, for example I have setup ".1251" locale to correctly transform file names defined in Windows-1251 encoding to Unicode.
  • log
    • This element defines global file logger setup, well known logging levels set is used
  • mime-types
    • This element defines correspondence between file extension and MIME type send in 'Content-Type' header for this file. Types can be defined directly in this element's body or loaded from external file.
  • handlers
    • This element should contain all planned to use handlers registration. Each handler registration defines path to DLL/SO file to load and set of parameters that will be sent to handler initialization method. Handler in ahttp library - it is a plugin that can perform processing of defined file types, like ISAPI extension in IIS or HttpHandler in ASP.NET.
  • modules
    • This element should contain all planned to use modules registration. Each module registration defines path to DLL/SO file to load and set of parameters that will be sent to module initialization method. Module in ahttp library - it is a plugin that can contain a set of callbacks which will be used at defined HTTP request processing end-points like HttpModule in ASP.NET. At present, the following events for module are supported: ModuleCallbackOnRequestBegin, ModuleCallbackOnRequestResolve, ModuleCallbackOnRequestMapHandler, ModuleCallbackOnResponsePreSendHeaders, ModuleCallbackOnResponsePreSendContent, ModuleCallbackOnResponseEnd.

Virtual directory setup - directory element. Each virtual directory can be defined by absolute FS path ('path' attribute) or by relative path from parent's directory ('relative-path' attribute).

  • name
    • Mandatory attribute - used to build directories tree from server's root
  • path
    • Used to setup virtual directory absolute FS path
  • relative-path
    • Used to setup virtual directory relative FS path
  • virtual-path
    • Defines virtual path of directory
  • max-request-size
    • Optional attribute - defines maximal HTTP request size that can be processed by server. Default value - 2097152 bytes
  • enable-parent-path-access
    • Optional attribute - used to deny access to parent directory from mapPath method. Default value - 'false'
  • browsing-enabled
    • Enables directory browsing mode. "header-template", "parent-directory-template", "directory-template", "virtual-directory-template", "file-template" and "footer-template" used to format directory content HTML
  • handlers
    • This element defines ahttp handlers setup for current directory, can contain following elements: 'add', 'remove', 'clear', 'register'. All handlers registered for parent directory are applied to all children by default.

Complete Sample Code of a Very Simple Server

C++
// globals
namespace Global
{
    aconnect::string settingsFilePath;
         ahttp::HttpServerSettings globalSettings;

    aconnect::BackgroundFileLogger logger;
    aconnect::Server httpServer;
}

void processException (aconnect::string_constptr message, int exitCode) {
    std::cerr << "Unrecorable error caught: " << message << std::endl;
    exit (exitCode);
}

int main (int argc, char* args[])
{
    using namespace aconnect;
    namespace fs = boost::filesystem;

    if (argc < 2) {
        std::cerr << "Usage: " << args[0] <<  " <server-config-file />" << std::endl;
    }

    Global::settingsFilePath = args[1];
    string appPath = aconnect::util::getAppLocation (args[0]);

    try
    {
        Global::globalSettings.setAppLocaton ( fs::path
           (Global::appPath).remove_leaf().directory_string().c_str() );
        Global::globalSettings.load ( Global::settingsFilePath.c_str() );

    } catch (std::exception &ex) {
        processException (ex.what(), 1);
    } catch (...) {
        processException
        ("Unknown exception caught at settings loading", 1);
    }

    try
    {
        // create global logger
        string logFileTemplate = Global::globalSettings.logFileTemplate();
        Global::globalSettings.updateAppLocationInPath (logFileTemplate);
        fs::path logFilesDir = fs::path
            (logFileTemplate, fs::native).branch_path();
        if (!fs::exists (logFilesDir))
            fs::create_directories(logFilesDir);

        Global::logger.init (Global::globalSettings.logLevel(),
        logFileTemplate.c_str(),
        Global::globalSettings.maxLogFileSize());

    } catch (std::exception &ex) {
        processException (ex.what(), 2);
    } catch (...) {
        processException
        ("Unknown exception caught at logger creation", 2);
    }

    Global::globalSettings.setLogger ( &Global::logger);
    // init ahttp library
    ahttp::HttpServer::init ( &Global::globalSettings);

    try
    {
        Global::globalSettings.initPlugins(ahttp::PluginModule);
        Global::globalSettings.initPlugins(ahttp::PluginHandler);

        Global::httpServer.setLog ( &Global::logger);
        Global::httpServer.init (Global::globalSettings.port(),
            ahttp::HttpServer::processConnection,
            Global::globalSettings.serverSettings());

        Global::httpServer.start (true);

    } catch (std::exception &ex) {
        processException (ex.what(), 3);
    } catch (...) {
        processException
        ("Unknown exception caught at server startup", 3);
    }

    return 0;
}

See more details in library code - I tried my best to write all simple code.

Points of Interest

While working on this project, I realized that C++ is still the best variant for high-load server-side services. The strongly typed language provides the ability to write short but fast and powerful constructions like this...

C++
template <typename />
class ScopedMemberPointerGuard {
public:
    ScopedMemberPointerGuard (T* obj, F T::* member, F initialValue ) :
        _obj (obj), _member (member) {
            _obj->*_member = initialValue;
    }

    ~ScopedMemberPointerGuard () {
        _obj->*_member = 0;
    }

private:
    T* _obj;
    F T::* _member;
};

... cannot be forgotten by developers. Working on this project, I have got great experience in ISAPI extensions internal architecture, ASP.NET HTTP runtime programming in native environment - all these skills are not trivial programming tasks and can be effectively used in professional work.

Planned Improvements

aconnect library:

  • UDP server support
  • TCP/UDP client
  • Cache class (stores ICacheable<T>) - very challenging task - I am planning to create something like .NET Cache in pure C++.

ahttp library and plugins:

  • Implement CGI/FastCGI handler (like handler_isapi - multiple mappings)
  • Implement found-targets cache in HttpServer (using aconnect::Cache)
  • "gzip/deflate" content encoding support, module (with zlib or boost::iostreams)
  • HTTP client
  • In-memory cache module for static content
  • Introduce Django support for Python handler

Known Compatibility Issues

  1. Using of c:\PHP\php5isapi.dll through ISAPI handler (tested with PHP 5.2.5 and PHP 4.3.10) can be the cause of "Access violation" exception at server stopping (::FreeLibrary call)
  2. ASP.NET handler tested only with .NET Framework v2.0.50727 (Microsoft .NET Framework 3.5 SP1 installed)

Version History

Ver. 0.15

  • aconnect: Optimized worker threads pooling mechanism
  • ahttp: Server messages loading from XML (localization)
  • ahttp: Implemented handlers unloading mechanism (destroyHandlers)
  • ahttp: Implemented stable multithreaded Python handler version
  • Many code refactorings performed (class members, namespaces, error handling)
  • Windows: Visual Solution solution converted to Visual Studio 2008

Ver. 0.16

  • ahttp: Defined handler ID and process request by correct DLL - used in ISAPI handler, can be used in other handlers which can be linked to several extensions
  • ahttp: VirtualPath renamed to InitialVirtualPath, MappedVirtualPath to VirtualPath to make them consistent
  • ahttp: Added using aconnect::string and related types into ahttp namespace
  • ahttp: [Windows] ISAPI extensions wrapper (handler), tested with PHP ISAPI extension.
  • ahttp: Implemented SERVER VARIABLES collection support in HttpContext
  • ahttp: Implemented safe handlers unloading (::FreeLibrary/dlclose)
  • aconnect: Implemented ability to start server on defined IP, loaded from config (default value: 0.0.0.0 - INETADDR_ANY).

Ver. 0.17

  • aconnect: Implemented background file logger: collect messages to write and write them in background (avoid locking on file writing)
  • ahttp: Redeveloped handlers registration (<unregister><clear>tags added to simplify handlers management), added ignored extensions list in "*" mode (.js, .gif... - for MVC handlers)
  • ahttp: Implemented case-insensitive request/response headers save/load (useful for handlers)
  • ahttp: [Windows] Implemented ASP.NET handler (Managed C++)
  • ahttp: Developed multiple extensions mapping for handler in register@ext and in handler@default-ext (list of ext linked to named handler), now ASP.NET MVC can be setup by following settings in <directory>element:
    XML
    <handlers>
    	<register name="handler_aspnet" ext="*"/>
    	<unregister name="handler_aspnet" ext=".gif; .js; .css; .jpg; .png"/>
    </handlers>
  • ahttp: Added max-request-size setting for directory (deny requests with larger content-length, send 413 HTPP error)

Ver. 0.18

  • ahttp: Implemented "If-Modified-Since" header support
  • ahttp: Added support of "Accept-Ranges" header used for partial content download

Ver. 0.19

  • ahttp: Developed server modules support (like HttpModule in .NET: onRequestBegin, onRequestMapHandler, onResponsePreSendHeaders, onResponsePreSendContent, onResponseEnd)
  • ahttp: Basic authentication (module)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)