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






4.78/5 (23投票s)
本文描述了 ahttpserver 演进的结果 - ASP.NET 处理程序的实现以及许多架构改进
- 下载 ahttpserver 和 ASP.NET MVC 演示(Windows 版本,需要 .NET 2.0)- 510.65 KB
备注- 要启动服务器,请进入服务器目录并运行 'ahttpserver.exe start'。
- 要查看服务器统计信息,请运行 'ahttpserver.exe stat'。
- 要停止服务器,请运行 'ahttpserver.exe stop'。
- 下载源代码(VS.NET 2008 解决方案/gcc makefile)- 358.09 KB
备注- 项目基于 Boost 1.34.1 创建 - 您应该安装它才能编译解决方案。
- 要编译 python_handler,您需要安装 Python 2.5。
- 要在 VS.NET 2008 下调试 ahttpserver,请设置 ahttpserver->属性->调试->命令行参数:run。
引言
本文描述了 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
。目前,模块支持以下事件:ModuleCallbackOnRequestBegin
、ModuleCallbackOnRequestResolve
、ModuleCallbackOnRequestMapHandler
、ModuleCallbackOnResponsePreSendHeaders
、ModuleCallbackOnResponsePreSendContent
、ModuleCallbackOnResponseEnd
。
虚拟目录设置 - 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 支持
已知兼容性问题
- 通过 ISAPI 处理程序使用 c:\PHP\php5isapi.dll(使用 PHP 5.2.5 和 PHP 4.3.10 进行测试)可能会在服务器停止时(调用
::FreeLibrary
)导致“访问冲突”异常。 - 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 处理程序中使用,可用于可链接到多个扩展的其他处理程序ahttp
:VirtualPath
更名为InitialVirtualPath
,MappedVirtualPath
更名为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 中的HttpModule
:onRequestBegin
、onRequestMapHandler
、onResponsePreSendHeaders
、onResponsePreSendContent
、onResponseEnd
)ahttp
:基本身份验证(模块)