65.9K
CodeProject 正在变化。 阅读更多。
Home

小巧可靠的 C++ HTTP 服务器,完整支持 ASP.NET

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (23投票s)

2009 年 2 月 5 日

CPOL

8分钟阅读

viewsIcon

76851

downloadIcon

1674

本文描述了 ahttpserver 演进的结果 - ASP.NET 处理程序的实现以及许多架构改进

引言

本文描述了 ahttp 库自首次发布以来的更改。我决定上传一篇新文章,因为服务器代码经过了大量修改,目前它几乎是一个生产就绪的项目。

当前服务器版本可以轻松地用作 ASP.NET 应用程序开发中的 IIS 替代品。服务器支持通配符 ('.*') 映射,并维护排除异常列表 - 此功能可用于设置 ASP.NET MVC 应用程序(请参阅所附演示中的示例)。

在首次分享 ahttp 版本(大约一年前)之后,我继续研究这个项目。这次开发的主要目标保持不变——研究和使用 C++ 中可用的最新功能,应用不同的已知设计实践来创建稳定且可扩展的应用程序架构。这个项目是我在业余时间开发的,与我的直接工作无关——在办公室生活中,我主要从事 Web 应用程序(ASP.NET、JavaScript、jQuery,最近我参与了一个基于 ASP.NET MVC 的项目)。

目前,ahttp 项目包含三个主要部分

  • aconnect 静态库:包含多线程 TCP 服务器实现、文件记录器以及大量实用功能 - TCP 套接字控制、字符串处理算法、日期/时间函数、加密
  • ahttplib 静态库:HttpServer 定义(详见第一篇文章)和所有 HTTP 请求解析/处理功能,服务器设置加载代码
  • ahttpserver:简单的 HTTP 服务器应用程序

服务器仍然支持 Windows 和 Linux 平台(仅在 Ubuntu 上测试过)。

为了扩展服务器功能,开发了一套插件

  • handler_aspnet - ASP.NET 应用程序支持(仅在 Windows 下可用)。此插件的通用架构复制自 .NET Cassini 服务器
  • handler_isapi - IIS ISAPI 扩展包装器 - 使用此包装器,ahttpserver 可以利用已开发的扩展来支持不同的脚本引擎。此处理程序可以正确使用 PHP 4 和 5,我尝试使用 ASP 和 ASP.NET ISAPI 扩展,但它们都使用了 IIS 的未公开功能,无法加载到 ahttpserver
  • handler_python - Python 脚本支持,与服务器端 Python 的一般方法不同 - 此模块直接执行脚本
  • module_authbasic - 基本身份验证支持 - 提供两种类型的身份验证提供程序。服务器提供程序根据从文件加载的列表对用户进行身份验证,系统提供程序(目前仅在 Windows 下工作)根据操作系统对用户进行身份验证。

Using the Code

服务器/库架构

所有 HTTP 服务器内核代码都位于 ahttp 库中,以便可以将服务器嵌入到任何现有架构中,或者只使用服务器代码中必要的部分,例如 HTTP 请求解析。使用此库,开发人员可以创建可定制的 Web 服务器来处理特定的 HTTP 请求 - SOAP 请求、文件下载等。

项目中包含的完整独立 HTTP 服务器应用程序 (ahttpserver) 只有大约 15 KB 的源代码(当然不包括库代码)。因此,使用此库可以大大减少服务器端项目估算中的开发/原型设计工作。即使决定不将此库用作服务基础,也可以使用提供的源代码片段将其包含在自己的项目中。

要使 ahttp::HttpServer 工作,需要填充 HttpServerSettings 实例 - 即使对于简单的服务器,也有许多设置可以配置。因此,存储这些设置的首选位置是 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>    

设置文件的第一部分 - server 定义了 HTTP 服务器的启动/运行时行为:服务器端口(port 属性)、绑定到的 IP 地址(目前只支持 IPv4)。其他参数

  • 工作线程数
    • 线程池中的最大工作线程数
  • 启用池化
    • 定义服务器工作模式 - 单线程 (pooling-enabled = 'false') 或多线程
  • 工作线程生命周期
    • 工作线程释放超时(秒)
  • 命令端口
    • ahttpserver 中用于打开额外的监听端口,以接收服务器控制命令(“start”、“stop”、“reload”)
  • 启用 Keep-Alive
    • 设置 HTTP Keep-Alive 模式
  • 服务器套接字超时
    • HTTP 服务器套接字读/写超时(秒)
  • 响应缓冲区大小
    • HTTP 响应缓冲区大小(字节)
  • 最大分块大小
    • 分块响应模式下的最大分块大小(字节)
  • 目录配置文件
    • 位于服务器虚拟目录中,用于加载默认文档列表、插件注册和服务器 URL 映射设置的就地文件名称(请参阅源代码包中的示例)
  • 消息文件
    • 服务器消息本地化文件
  • 上传目录
    • 全局上传目录,用于存储已提交文件的内容
  • locale
    • 重要设置 - 当此属性不为空时,服务器启动时将执行 setlocale (LC_CTYPE, localeStr.c_str())。区域设置可用于强制 mbstowcs 正确工作,例如,我设置了 ".1251" 区域设置以将 Windows-1251 编码中定义的文件名正确转换为 Unicode。
  • log
    • 此元素定义全局文件记录器设置,使用众所周知的日志级别集
  • MIME 类型
    • 此元素定义文件扩展名与发送到此文件的“Content-Type”头中的 MIME 类型之间的对应关系。类型可以直接在此元素主体中定义,也可以从外部文件加载。
  • handlers
    • 此元素应包含所有计划使用的处理程序注册。每个处理程序注册定义要加载的 DLL/SO 文件的路径以及将发送到处理程序初始化方法的参数集。ahttp 库中的处理程序是一种插件,可以对定义的 文件类型执行处理,就像 IIS 中的 ISAPI 扩展或 ASP.NET 中的 HttpHandler 一样。
  • 模块
    • 此元素应包含所有计划使用的模块注册。每个模块注册定义要加载的 DLL/SO 文件的路径以及将发送到模块初始化方法的参数集。ahttp 库中的模块是一种插件,可以包含一组回调,这些回调将在定义的 HTTP 请求处理端点处使用,例如 ASP.NET 中的 HttpModule。目前,模块支持以下事件:ModuleCallbackOnRequestBeginModuleCallbackOnRequestResolveModuleCallbackOnRequestMapHandlerModuleCallbackOnResponsePreSendHeadersModuleCallbackOnResponsePreSendContentModuleCallbackOnResponseEnd

虚拟目录设置 - directory 元素。每个虚拟目录可以通过绝对文件系统路径(“path”属性)或相对于父目录的路径(“relative-path”属性)来定义。

  • 名称
    • 必需属性 - 用于从服务器根目录构建目录树
  • 路径
    • 用于设置虚拟目录的绝对文件系统路径
  • 相对路径
    • 用于设置虚拟目录的相对文件系统路径
  • 虚拟路径
    • 定义目录的虚拟路径
  • 最大请求大小
    • 可选属性 - 定义服务器可处理的最大 HTTP 请求大小。默认值 - 2097152 字节
  • 启用父路径访问
    • 可选属性 - 用于拒绝从 mapPath 方法访问父目录。默认值 - 'false'
  • 启用浏览
    • 启用目录浏览模式。“header-template”、“parent-directory-template”、“directory-template”、“virtual-directory-template”、“file-template”和“footer-template”用于格式化目录内容的 HTML
  • handlers
    • 此元素定义当前目录的 ahttp 处理程序设置,可以包含以下元素:“add”、“remove”、“clear”、“register”。默认情况下,所有为父目录注册的处理程序都应用于所有子目录。

一个非常简单服务器的完整示例代码

    // 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] <<  " " << 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;
    }    

更多细节请参阅库代码 - 我尽力编写了所有简单代码。

关注点

在从事这个项目时,我意识到 C++ 仍然是高负载服务器端服务的最佳选择。强类型语言提供了编写像这样简短但快速且强大的结构的能力……

    template 
    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;
    };    

...不能被开发人员遗忘。通过这个项目,我在 ISAPI 扩展的内部架构、在原生环境中进行 ASP.NET HTTP 运行时编程方面获得了丰富的经验——所有这些技能都不是简单的编程任务,可以在专业工作中有效地使用。

计划改进

aconnect

  • UDP 服务器支持
  • TCP/UDP 客户端
  • 缓存类(存储 ICacheable<T>)- 这是一个非常具有挑战性的任务 - 我计划用纯 C++ 创建类似 .NET 缓存的东西。

ahttp 库和插件

  • 实现 CGI/FastCGI 处理程序(类似于 handler_isapi - 多重映射)
  • HttpServer 中实现找到的目标缓存(使用 aconnect::Cache
  • "gzip/deflate" 内容编码支持,模块(使用 zlib 或 boost::iostreams
  • HTTP 客户端
  • 用于 静态 内容的内存中缓存模块
  • 为 Python 处理程序引入 Django 支持

已知兼容性问题

  1. 通过 ISAPI 处理程序使用 c:\PHP\php5isapi.dll(使用 PHP 5.2.5 和 PHP 4.3.10 进行测试)可能会在服务器停止时(调用 ::FreeLibrary)导致“访问冲突”异常。
  2. ASP.NET 处理程序仅在 .NET Framework v2.0.50727(已安装 Microsoft .NET Framework 3.5 SP1)下测试过

版本历史

版本 0.15

  • aconnect:优化了工作线程池机制
  • ahttp:从 XML 加载服务器消息(本地化)
  • ahttp:实现了处理程序卸载机制 (destroyHandlers)
  • ahttp:实现了稳定的多线程 Python 处理程序版本
  • 进行了许多代码重构(类成员、命名空间、错误处理)
  • Windows:Visual Solution 解决方案转换为 Visual Studio 2008

版本 0.16

  • ahttp:定义了处理程序 ID 并由正确的 DLL 处理请求 - 在 ISAPI 处理程序中使用,可用于可链接到多个扩展的其他处理程序
  • ahttpVirtualPath 更名为 InitialVirtualPathMappedVirtualPath 更名为 VirtualPath 以使其保持一致
  • ahttp:将 aconnect::string 和相关类型添加到 ahttp 命名空间中
  • ahttp:[Windows] ISAPI 扩展包装器(处理程序),已使用 PHP ISAPI 扩展进行测试。
  • ahttp:在 HttpContext 中实现了 SERVER VARIABLES 集合支持
  • ahttp:实现了安全的处理程序卸载 (::FreeLibrary/dlclose)
  • aconnect:实现了在定义的 IP 上启动服务器的能力,从配置中加载(默认值:0.0.0.0 - INETADDR_ANY)。

版本 0.17

  • aconnect:实现了后台文件日志记录器:收集要写入的消息并在后台写入(避免文件写入时的锁定)
  • ahttp:重新开发了处理程序注册(添加了标签以简化处理程序管理),在“*”模式下添加了忽略的扩展列表(.js.gif... - 用于 MVC 处理程序)
  • ahttp:实现了不区分大小写的请求/响应头保存/加载(对处理程序有用)
  • ahttp:[Windows] 实现了 ASP.NET 处理程序(托管 C++)
  • ahttp:为 register@ext 和 handler@default-ext 中的处理程序开发了多个扩展映射(链接到命名处理程序的扩展列表),现在可以通过以下设置设置 ASP.NET MVC:element
    <handlers>
    	<register name="handler_aspnet" ext="*"/>
    	<unregister name="handler_aspnet" ext=".gif; .js; .css; .jpg; .png"/>
    </handlers>
  • ahttp:为目录添加了 max-request-size 设置(拒绝内容长度更大的请求,发送 413 HTPP 错误)

版本 0.18

  • ahttp:实现了“If-Modified-Since”头支持
  • ahttp:添加了对用于部分内容下载的“Accept-Ranges”头的支持

版本 0.19

  • ahttp:开发了服务器模块支持(类似于 .NET 中的 HttpModuleonRequestBeginonRequestMapHandleronResponsePreSendHeadersonResponsePreSendContentonResponseEnd
  • ahttp:基本身份验证(模块)
© . All rights reserved.