Marlin:逃离 IIS






4.92/5 (20投票s)
完整的 Web 服务器和 Web 客户端,带 SOAP/JSON 消息传递框架
引言
本文档介绍 Marlin Web 服务器库。它的结构以及如何在您的程序中使用它。
“Marlin”组件“Web 服务器和 Web 客户端”是用 C++ 构建的,围绕一系列通用类,这些类负责最佳性能,并且旨在使服务器和客户端部分可以插入 C++ 项目中。
构建此库的主要原因是扩展现有的 Web 服务器,使其具备基本的 HTTPS 功能,如 SSL 和 TLS 连接,以及高级身份验证功能,如摘要身份验证和 Kerberos 身份验证。我没有重新构建所有这些组件,而是选择使用通用的现有 Microsoft 组件“HTTP-Server API 2.0”和“WinHTTP API 5.1”。
选择 C++ 来构建这些组件——而不是 .NET 技术——是出于对组件性能与 WCF 服务进行比较的考虑。通常,与 .NET 中的 WCF 实现相比,这些组件的速度快很多倍。特别是如果您配置了各种 W3C 标准,如消息签名、安全加密和可靠消息传递。
另一个原因是需要在远程桌面(Citrix!)环境上运行 Web 服务。这显然无法通过 IIS ISAPI 框架来实现。尽管 IIS 7.0 及更高版本通过新的集成请求管道做了很多改进,但在每个桌面上启用 IIS 服务器都不是一个可行的选择。背景。
名称
这是一条白马林鱼的照片,它是大西洋常见的马林鱼之一。它是大西洋中最快的鱼,而且非常神秘。如果你成功地用鱼线钓到一条,它会在深处“看不见地”挣扎。就像一个快速的 Web 服务器一样 :-)
组件包括
一系列通用类,您可以借助它们来构建应用程序的功能。这些通用类被分组在一个公共目录中,以便服务器应用程序和客户端应用程序都可以使用它们。此外,还创建了示例 Web 服务器和 Web 客户端程序。这些程序用作单元测试框架,同时也是方便您的编码示例。这将产生以下目录:
Marlin | 包含通用类的目录 |
MarlinServer | 包含示例 Web 服务器的目录 |
MarlinClient | 包含示例客户端程序的目录 |
HTTPManager | 包含 HTTP 管理应用程序的目录 |
要使用这些组件和示例,您只需在 MarlinServer 和 MarlinClient 目录中的 Microsoft Visual Studio(2015 版)中打开解决方案文件即可。两个解决方案都包含一个测试项目和一个包含 Marlin 目录中通用类子集的目录。
有关如何编译和在您的机器上设置服务器的说明,请参阅本文档后面的“运行示例”部分。
架构概述
在此图中,您可以看到 Marlin Web 服务器和 Web 客户端组件的总体架构概述。
关于如何使用文章或代码的简要说明。类名、方法和属性,任何技巧或窍门。
编程模型:客户端
核心编程模型是 `HTTPClient` 对象。消息(`HTTPMessage` 或 `SOAPMessage`)通过重载的“`Send`”方法发送到此对象。此方法返回 TRUE 状态后,消息中就包含服务器的应答。然后,您可以从对象中提取服务器的应答。示例 1 展示了如何做到这一点。
// // Example 1 // #include "HTTPClient.h" #include "HTTPMessage.h" void function() { HTTPClient client; HTTPMessage msg(http_get,"http://www.organisation.com/some_file.html"); if(client.Send(msg)) { // Success. Get the result msg.GetFileBuffer().SetFileName("C:\tmp\MyLocalFile.html"); msg.GetFileBuffer().WriteFile(); } else { // Error CString text; client.GetError(text); printf("Error getting file: %s\n",text); } }
当然,您可以通过重写客户端和消息的默认设置,并使用一系列 `Set*` 方法来扩展此示例。要使用 Web 服务,您只需将通用的 `HTTPMessage` 对象替换为更专业的 `SOAPMessage` 对象即可。向 `SOAPMessage` 添加一些参数,然后通过客户端发送到您的服务接口。如果“`Send()`”方法返回 TRUE,对象将包含服务器的应答。这在示例 2 中显示。
// Example 2 // #include "HTTPClient.h" #include "SOAPMessage.h" HTTPClient client; SOAPMessage msg("http://mycompany.com/mynamespace/" ,"TestMethodOne" ,SOAP_12 ,"https://mycompany.com/TestInterfaceOne/"); // Fill the message with parameter info msg.SetParameter("ParamOne","Value1"); msg.SetParameter("ParamTwo","Value2"); if(client.Send(msg)) { // Success. Get the result from the object CString result = msg.GetParameter("ResultParameter"); ... } else { // Error CString text; client.GetError(text); printf("Error getting file: %s\n",text); }
当然,通过这种方式,我们一次只能通过我们的 HTTP(S) 通道发送一条 SOAP 消息。如果我们想添加 WS-Security、WS-ReliableMessaging 或 WSDL 支持,我们不能立即做到。但是请稍等,通过将“`HTTPClient`”对象替换为“`WebServiceClient`”对象,这些协议即可开箱即用!只需使用 WebServiceClient 的“`Send()`”方法即可处理。我们只需要为此目的构造一个带有所需合同和 URL 的 WebServiceClient 对象。请参阅示例 3。
// Example 3 // #include "WebServiceClient.h" #include "SOAPMessage.h" WebServiceClient client("http://mycompany.com/mynamespace/" ,"https://mycompany.com/TestInterfaceOne/Reliable/"); SOAPMessage msg("https://mycompany.com/mynamespace/","TestMethodOne") client.SetReliable(true,RELIABLE_ONCE); // Fill the message with parameter info msg.SetParameter("ParamOne","Value1"); msg.SetParameter("ParamTwo","Value2"); if(client.Send(msg)) { // Success after WS-Reliable protocol. Get the result from the object CString result = msg.GetParameter("ResultParameter"); ... } else { // Some error handling CString text; client.GetError(text); printf("Error getting file: %s\n",text); }
编程模型:服务器端
核心编程模型是 sitehandler。每个 handler 是一个对象类,它“原则上”处理一个 HTTP 方法。因此,有一个 handler 用于 HTTP 协议的 GET 和 PUT 命令。每个 handler 都有一个中心方法“Handle”,并将当前接收到的消息作为参数。从该对象中提取信息,然后使用“Reset”方法将该对象重置为 null 状态并填充应答。无需调用 `Send()` 或 `Answer()` 方法将结果发送回调用者。这将在 handler 结束时由框架自动完成。下面是一个(非常)简单的服务 GET 请求的示例。请注意:不要在家尝试这个!!
// Example 4 // #include "SiteHandlerGet.h" #define webroot "C:\\inetpub\\myapp\\" bool SiteHandelerGet::Handle(HTTPMessage* p_message) { CString filename = webroot + p_message->GetAbsolutePath(); p_message.Reset(); p_message.GetFileBuffer.SetFileName(filename); }
要服务基本的 SOAP 请求,您只需以相同的方式使用 SOAP handler。`SiteHandlerSoap` 有一个“Handle”方法的重写,专门用于服务 SOAP 请求。同样,不需要将应答发回。这将在 handler 结束时完成。下面是一个服务示例 2 请求的 handler。
// Example 5 // #include "SiteHandlerSoap" bool SiteHandlerSoap::Handle(SOAPMessage* p_message) { // Getting the info CString paramOne = p_message.GetParameter("ParamOne"); CString paramTwo = p_message.GetParameter("ParamTwo"); // Reseting the object p_message.Reset(); // Doing our logic! if(paramOne == "Value1" && paramTwo == "Value2") { P_message.SetParameter("ResultParameter","I declare this OK"); } }
那么,我们如何在服务器端创建一个 HTTP handler 呢?有几种方法,我们在这里展示。首先,我们需要声明一个 `HTTPServer` 对象。这很简单。然后创建一个 `HTTPSite`,设置该站点的几个参数,然后“启动”它。
您设置的参数之一是 SiteHandler,您首先声明它。
这是一个更详细的示例,它扩展了前面的示例 5。
// Example 6 // #include "HTTPServer.h" #include "HTTPSite.h" #include "SiteHandlerSoap" class SiteHandlerSoapForMe : public SiteHandlerSoap { protected: Bool Handle(HTTPMessage* p_message); } bool SiteHandlerSoapForMe::Handle(SOAPMessage* p_message) { // Getting the info CString paramOne = p_message.GetParameter("ParamOne"); CString paramTwo = p_message.GetParameter("ParamTwo"); // Reseting the object p_message.Reset(); // Doing our logic! if(paramOne == "Value1" && paramTwo == "Value2") { P_message.SetParameter("ResultParameter","I declare this OK"); } } void CreatingTheService() { CString url("https://mycompany.com/TestInterfaceOne/"); HTTPServer server("TestInterfaceOne"); HTTPSite* mysite = server->CreateSite(URLPRE_Strong,true,443,url); // Setting the SOAP handler for this site site->SetHandler(http_post,new SiteHandlerSoapForMe()); site->AddContentType("application/soap+xml"); // Start the site explicitly if(site->StartSite()) { printf("Site started correctly: %s\n",url); } else { ++error; printf("ERROR STARTING SITE: %s\n",url); } // Now start the server running (in it's own thread!) server.Run(); // Or start the server in this thread (commented out) // Server.RunHTTPServer(); }
与客户端编程模型一样,我们也可以用 `WebServiceServer` 对象替换 `HTTPServer` 对象。这使我们能够处理 WS-Security、WS-ReliableMessaging 和 WSDL 检查。服务器端在启动对象时也会写入 WSDL 文件,除非我们使用“`SetGenerateWsdl(false)`”告诉它不要这样做。
操作方法如下:
// Example 7 // #include "WebServiceServer.h" void CreatingTheService() { CString url("https://mycompany.com/TestInterfaceOne/"); CString webroot("C:\\inetpub\\"); CString namespace("http://mycompany.com/mynamespace/"); WebServiceServer server("TestOne",webroot,url,URLPRE_STRING,namespace,10); // Create our site HTTPSite* mysite = server->CreateSite(URLPRE_Strong,true,443,url); // Setting the SOAP handler for this site site->SetSoapHandler(http_post,new SiteHandlerSoapForMe()); // Running the server server.Run() }
整合
但是,如果我们想运行一组 Web 服务呢?比如一个服务接口,并将其描述在 WSDL 文件中供世界知晓?嗯:这也可以在 `WebServiceServer` 中完成。要启用 WSDL 功能,您必须首先在服务器的 WSDL 缓存中注册所有服务。这可以通过向此缓存提交传入和传出的 SOAP 消息的副本(SOAPMessage)来完成。同时,您可以做两件事:
- 为 WSDL 的每个参数添加额外信息,用于“必需/可选”状态,以及元素的排序可选性。
- 为每条消息提供服务号,供接收分派器使用。请记住这一点,并观察编程时发生的情况。
下面是测试示例的摘录,我首先从 Web 服务服务器派生一个类,并为该类定义了三个服务(一个、两个和三个)。
// Example 8a // #include "WebServiceServer.h" #include "SiteHandlerSoap.h" #define CONTRACT_MF 1 // First #define CONTRACT_MS 2 // Second #define CONTRACT_MT 3 // Third // DERIVED CLASS FROM WebServiceServer class TestContract: public WebServiceServer { public: TestContract(CString p_name ,CString p_webroot ,CString p_url ,PrefixType p_channelType ,CString p_targetNamespace ,unsigned p_maxThreads); protected: WEBSERVICE_MAP; // Using a WEBSERVICE mapping // Declare all our webservice call names // which will translate in the On.... methods WEBSERVICE_DECLARE(MarlinFirst) WEBSERVICE_DECLARE(MarlinSecond) WEBSERVICE_DECLARE(MarlinThird) // Our functionality CString Translation(CString p_language,CString p_translation,CString p_word); // Set input/output languages CString m_language; CString m_translation; };
在实例化了这个类的对象之后,我们可以通过“AddOperation”将传入和传出的 SOAP 消息添加到 `WebServiceServer` 的 WSDL 缓存中。
本示例中的操作编号分别为 1 (CONTRACT_MF)、2 (CONTRACT_MS) 和 3 (CONTRACT_MT)。
// Example 8b // #include "WebServiceServer.h" ////////////////////////////////////////////////////////////////////////// // // PREPARING OUR WSDL, This is what will fill the WSDL file // void AddOperations(TestContract& p_server,CString p_contract) { // Defining the names of the operations CString first ("MarlinFirst"); CString second("MarlinSecond"); CString third ("MarlinThird"); CString respFirst ("ResponseFirst"); CString respSecond("ResponseSecond"); CString respThird ("ResponseThird"); // Defining input and output messages for the operations SOAPMessage input1 (p_contract,first); SOAPMessage output1(p_contract,respFirst); SOAPMessage input2 (p_contract,second); SOAPMessage output2(p_contract,respSecond); SOAPMessage input3 (p_contract,third); SOAPMessage output3(p_contract,respThird); // Defining the parameters for all the operations // First: Getting an accepted language input1 .AddElement(NULL,"Language",WSDL_Mandatory | XDT_String, "string"); output1.AddElement(NULL,"Accepted",WSDL_Mandatory | XDT_Boolean,"bool"); // Second: Getting an accepted translation input2 .AddElement(NULL,"Translation",WSDL_Mandatory | XDT_String, "string"); output2.AddElement(NULL,"CanDo", WSDL_Mandatory | XDT_Boolean,"bool"); // Third Getting the answer input3 .AddElement(NULL,"WordToTranslate",WSDL_Mandatory | XDT_String,"string"); output3.AddElement(NULL,"TranslatedWord", WSDL_Optional | XDT_String,"string"); // Putting the operations in the WSDL Cache p_server.AddOperation(CONTRACT_MF,first, &input1,&output1); p_server.AddOperation(CONTRACT_MS,second,&input2,&output2); p_server.AddOperation(CONTRACT_MT,third, &input3,&output3); }
将操作添加到派生的 `WebServiceServer` 类中将自动生成 WSDL 文件,除非我们将其设置为 false。
在定义了接口之后,我们现在可以定义操作本身。这是简单的一部分,我们在这里定义我们的 handler。
// Example 8c // #include "WebServiceServer.h" // Implementation of the TestContract class TestContract::TestContract(CString p_name ,CString p_webroot ,CString p_url ,PrefixType p_channelType ,CString p_targetNamespace ,unsigned p_maxThreads) :WebServiceServer(p_name,p_webroot,p_url,p_channelType,p_targetNamespace,p_maxThreads) { } // Mapping corresponding to the AddOperation of the WSDL WEBSERVICE_MAP_BEGIN(TestContract) WEBSERVICE(CONTRACT_MF,MarlinFirst) WEBSERVICE(CONTRACT_MS,MarlinSecond) WEBSERVICE(CONTRACT_MT,MarlinThird) WEBSERVICE_MAP_END ////////////////////////////////////////////////////////////////////////// // // HERE ARE THE SERVICE HANDLERS!! // Derived from the definition above in the WEBSERVICE_MAP // ////////////////////////////////////////////////////////////////////////// void TestContract::OnMarlinFirst(int p_code,SOAPMessage* p_message) { ASSERT(p_code == CONTRACT_MF); m_language = p_message->GetParameter("Language"); printf("\n"); printf("Setting base language to: %s\n",(LPCTSTR)m_language); // Reset message and set answering parameters p_message->Reset(); p_message->SetParameter("Accepted",m_language == "Dutch"); } ... more handlers... See "TestContract.cpp"
经过这番抽象之后,您只需为每个服务调用编写一个“OnXxxxxx” handler,使用输入参数,重置消息并提供应答参数。其余的一切都由 Marlin 框架处理。
编程模型概述
在详细解释了编程方式之后,让我们来看看如何将其纳入一个编程思维模型。理想情况下,您通过调用构造函数(new SOAPMessage)构造一个 SOAP 消息,然后将该消息发送到 HTTPClient。调用返回后,您将收到一个空指针或一个应答。
该框架负责将您的 SOAP 消息构建成 HTTPMessage,通过 HTTP(S) 内网/互联网将其传输到您的服务器,在那里进行处理,然后将消息返回给您。
注意:在本文档和下图中的任何地方看到“SOAP”一词时,您也可以理解为“JSON”。
运行示例
下载源代码后,运行示例的步骤如下:
- 启动 Visual Studio 2015(最低要求);
- 加载“HTTPManager”项目并首先编译它,以便我们可以在机器上设置服务器。建议编译“debug | x64”变体;
- 启动 HTTPManager 并注册站点“https://:1200/MarlinTest/”(请参阅下图如何操作);
启动后,您应该:
- 按下“Listen on IP”按钮,确保您的机器会监听 IP 流量;
- 为端口号输入“1200”。所有测试都在 IP 端口 1200 上运行;
- 输入基本路径“/MarlinTest”。所有测试都在该站点的子站点上运行;
- 按下“Create”按钮来注册“http://+:1200/MarlinTest”前缀监听器;
- 关闭 HTTPManager 程序,返回 Visual Studio;
- 加载“MarlinServer”项目并进行编译;
- 启动第二个 Visual Studio,加载并编译“MarlinClient”项目;
- 现在首先启动 MarlinServer,并检查所有站点是否都已注册;
- 现在启动 MarlinClient,如果所有测试都令人满意,它将以“Yipee!!”结束。
为什么不使用 IIS 和 ISAPI?
但是,为什么不在我们可以正确管理它们的应用程序中使用 IIS,而无需“偷偷摸摸地”引入它们呢?适用于 C 或 C++ 的原生应用程序的 ISAPI 编程模型存在一些明显的限制,这些限制源于 IIS 的管理模型。这些限制如下所示:
- 最初,ISAPI 模型非常简单,只有一个模块 handler,并且可以“读取”传入的 http 流量并“写入”应答。幸运的是,在 7.0 版本出现时,该模型得到了扩展,但仍然非常有限。HTTP Server API 的一个优点是可以选择写入方式:一次性写入、分块写入,或者仅通过指向文件名来写入,而无需打开和读/写文件;
- ISAPI 模型不保证我们的调用会结束在同一个进程中,甚至同一个物理机器上。这使得有必要将一个会话的完整会话状态保存到外部机器或数据库,并在每次调用时重新读取会话状态。这会减慢基于会话的应用程序的服务器处理速度。特别是那些大量进行短调用活动的应用程序。
- 使用 ISAPI 意味着您必须扫描每个请求。当在一台机器上组合多个应用程序时,应用程序的性能会严重下降,因为所有应用程序都必须扫描平均 ½ *(所有应用程序的所有请求的总和)。这导致了安装说明,要求将每个应用程序安装在其自己的机器上。从而提高了总体拥有成本(TOC);
- ISAPI 扩展模块接口的设计,嗯……就是一个用于*扩展*的接口。不是真正的东西。您可以编写扩展接口或网站,但如果您想让它成为事物本身,您将面临一段艰难的时期。
历史
很久以前,我需要一个能够在 Citrix 桌面上运行的 Web 服务器。额外的要求是,不应该有管理员费心给我权限来运行 IIS 或 Apache 等 Web 服务器。我想要的是一个 Web 服务器,我可以将其“偷偷”放入一个更大的应用程序的角落。这样,应用程序就可以在相对自由的环境中进行通信,并且没有管理开销。
经过长时间的搜索,我选择了 Elmue 在 CodeProject 上找到的“Universal TCP Socket”程序(https://codeproject.org.cn/Articles/34163/A-Universal-TCP-Socket-Class-for-Non-blocking-Serv)。这是一个非常好的产品,它具有异步处理传入的 TCP/IP 流量等功能。唯一需要的是实现完整的 HTTP 协议。我当时就这么做了。而且它在相当长的一段时间内运行良好。
但是时代变了。我是否不能满足在互联网上运行的愿望,而不仅仅是在内网?并且请问您能否确保服务器能理解常规的 SSL/TLS 和身份验证方法?
好吧,哪个正直的程序员不想呢?我已经想象自己正在探索 OpenSSL 库和各种加密方法,但是,等等……为什么还要费事呢,如果 Microsoft 还没有解决这个问题呢?经过彻底的 Bing/Google 搜索,我找到了并选择了 Microsoft 的“HTTP Server API”(https://msdn.microsoft.com/en-us/library/windows/desktop/aa364510(v=vs.85).aspx)。它免费包含在任何 MS-Windows 安装中,并且在 Windows Vista / Server 2008 之后,该库的 2.0 版本还负责 SSL/TLS 和身份验证。因此,我将该库的核心类从“Universal TCP Socket”移植到了“HTTP Server API 2.0”,它就成了此程序中的 HTTPServer。
好吧,确切地说:我犹豫了片刻。为什么不使用现有的框架,如 gSOAP(http://www.cs.fsu.edu/~engelen/soap.html)或 Microsoft 的新且有前途的“Casablanca”框架(https://casablanca.codeplex.com)?两者都处于稍高的抽象级别,并提供了所有必需的机制。但在试验了两者之后,我决定坚持当前的编程模型。gSoap 被证明相当晦涩难懂,而 Casablanca 在迁移到 codeplex 之前是一个不断变化的目标。
多年来,一直需要与(主要是 .NET 堆栈中的)其他程序进行通信。其中一些程序使用了“可靠消息传递接口”。因此,在“WebServiceServer”和“WebServiceClient”中添加了一个额外的抽象层。因为 Microsoft .NET 堆栈使用 WSDL 1.1 而不是像 Java 堆栈那样使用 2.0,所以我使用了 1.1 版本来支持 WSDL(抱歉,Java 程序员)。
从一开始,我就编写了 HTTPMessage 和 SOAPMessage 来抽象消息的发送和接收。HTTPMessage 负责文件(put 和 get),SOAPMessage 负责 SOAP XML 消息。所有站点都位于服务器的映射中。一段时间后,服务器扩展到越来越多的程序,很快就清楚需要一个额外的抽象层。这就是 HTTPSite 对象。服务器的许多设置和属性都移到了站点对象,以便不同的站点可以具有不同的设置。
这个 Web 服务器目前处于第三个版本。版本历史包含在文档目录的源代码中。本文档和 Visio 图中的架构概述也是如此。
修订历史
此项目的完整修订历史记录包含在源代码下载中。