ATL Server - Web 应用程序/Web 服务






4.87/5 (26投票s)
使用 ATL Server 类进行 Web 应用程序/Web 服务开发
引言
其他 ATL7 的更改已在 此处 和 此处 进行了介绍。首先,我们来回顾一下为简化 ATL Server 开发而添加的一些新类。这些类可以在任何需要的地方使用,不仅仅局限于 ATL Server 开发。加密 (CCryptProv, CCryptKey, CCryptKeyedHash 等)
- 加密参考 (MSDN)
哈希
CCryptProv prov; prov.Initialize(); char rand[256]; memset(rand, 0, sizeof(rand)); //generating cryptographically random bytes HRESULT hr = prov.GenRandom(sizeof(rand), (BYTE*)rand); if(hr != S_OK) return 0; ///////////////////////////////////////////// //hashing //TODO: input password from user. never hard code it like this! const char* szPassword = "#$%^!SDD%$^&%^&~"; char Hash1st[256]; DWORD dwSize=256; char Hash2nd[256]; memset(Hash1st, 0, sizeof(Hash1st)); memset(Hash2nd, 0, sizeof(Hash2nd)); CCryptKeyedHash hash; //use SHA-1 or any other such as MD5, HMAC, etc hash.Initialize(prov, CALG_SHA1, CCryptKey::EmptyKey, 0); // add data to hash hr = hash.AddString(szPassword); if(hr != S_OK) return 0; //get the hashed value hr = hash.GetValue((BYTE*)Hash1st, &dwSize); if(hr != S_OK) return 0; hash.Destroy(); // now we can check the hashed value against original whenever we want hash.Initialize(prov, CALG_SHA1, CCryptKey::EmptyKey, 0); //add data to hash hr = hash.AddString(szPassword); if(hr != S_OK) return 0; //get hashed value hr = hash.GetValue((BYTE*)Hash2nd, &dwSize); if(hr != S_OK) return 0; //hash values should matched if(memcmp(Hash1st, Hash2nd, sizeof(Hash1st)) == 0) ATLTRACE("\npassword matched.\n\n");
加密
const int ENCRYPT_BLOCK_SIZE = 64; //since TripleDES is used, which uses block cipher mode, //the data size must be a multiple of block size (8 for TripleDes) DWORD dwBlockLen = 2000 - 1000 % ENCRYPT_BLOCK_SIZE; ATLASSERT((dwBlockLen%8)==0); CHeapPtr<BYTE> buff; buff.Allocate(dwBlockLen); CCryptProv prov; //need MS_ENHANCED_PROV or MS_STRONG_PROV for TripleDES algo HRESULT hr = prov.Initialize(PROV_RSA_FULL, NULL, MS_STRONG_PROV); //TODO: create some file named testpln.txt with some data for example code // to work //encrypt file using TripleDES and based on password { CCryptDerivedKey derKey; //let's use md5 hash to hash the password for the file to be encrypted //normally you would use sha, this is only to show how you can easily use
//md5 if needed CCryptMD5Hash md5; hr = md5.Initialize(prov); //hash file password md5.AddString(szPassword); //initialize the key to use TripleDES and md5 based hash of the password hr = derKey.Initialize(prov,md5,CALG_3DES); //16 for AES, 8 for 3DES BYTE iv[8]; //generate cryptographically strong random bytes hr = prov.GenRandom(8, iv); //set above bytes as IV hr = derKey.SetIV(iv); //set mode to CBC hr = derKey.SetMode(CRYPT_MODE_CBC); //set padding hr = derKey.SetPadding(PKCS5_PADDING); FILE* pFilePl = fopen("testpln.txt", "rb"); //original file if(pFilePl == NULL) return 0; FILE* pFileEn = fopen("testenc.txt", "wb"); //to be encrypted file DWORD dwEncLen = 0; DWORD dwOrigBlockLen = dwBlockLen; while(!feof(pFilePl)) { DWORD nRead = fread(buff, 1, dwBlockLen, pFilePl); //read plain data DWORD nEnc = nRead; //encrypt data, data and size is returned back to us hr = derKey.Encrypt(feof(pFilePl), (BYTE*)buff, &nEnc, dwBlockLen); if(hr == HRESULT_FROM_WIN32(ERROR_MORE_DATA)) //error, buffer size // not enough { buff.Reallocate(nEnc); dwBlockLen = nEnc; hr = derKey.Encrypt(feof(pFilePl), (BYTE*)buff, &nRead,
dwBlockLen); nEnc = nRead; } if(hr != S_OK) break; fwrite(buff, 1, nEnc, pFileEn); //write encrypted data } fclose(pFileEn); fclose(pFilePl); //resize buffer back, in case it was changed dwBlockLen = dwOrigBlockLen; buff.Reallocate(dwBlockLen); } //decrypt same file using TripleDES and based on password { CCryptDerivedKey derKey; CCryptMD5Hash md5; hr = md5.Initialize(prov); //file password md5.AddString(szPassword); //initialize the key to use TripleDES and md5 based hash of the password hr = derKey.Initialize(prov,md5,CALG_3DES); //set IV, MODE, Padding like before FILE* pFileEn = fopen("testenc.txt", "rb"); //encrypted file FILE* pFileDec = fopen("testdec.txt", "wb");//to be decrypted file DWORD dwEncLen = 0; while(!feof(pFileEn)) { //read encrypted data DWORD nRead = fread(buff, 1, dwBlockLen, pFileEn); //decrypt data, data and size is returned back to us hr = derKey.Decrypt(feof(pFileEn), buff, &nRead); if(hr != S_OK) break; fwrite(buff, 1, nRead, pFileDec); //write decrypted data } fclose(pFileDec); fclose(pFileEn); }
正则表达式
正则表达式参考 (MSDN)CAtlRegExp<> regexp; CAtlREMatchContext<> mc; // match any line that starts with any number of digits, // has a dash, and ends with any number of digits if(regexp.Parse("^\\d+-\\d+$") == REPARSE_ERROR_OK) { const char* szNumDashNum="5663-4662"; if(regexp.Match(szNumDashNum, &mc)) { ATLTRACE("Matched"); } }
支持发送 SMTP 电子邮件
MIME & SMTP 参考 (MSDN)CoInitialize(0); { CSMTPConnection conn; conn.Connect("SMTP Server Address"); CMimeMessage msg; msg.SetSender(_T("sender@address")); msg.AddRecipient(_T("recepient@address")); msg.SetPriority(ATL_MIME_HIGH_PRIORITY); //msg.AttachFile(_T("filename")); msg.AddText(_T("message text")); msg.SetSubject(_T("Subject line")); conn.SendMessage(msg); } CoUninitialize();
编码
编码参考 (MSDN)BEncode, Base64Encode, UUEncode, QEncode, QPEncode, AtlHexEncode。
CString sSource = "some string"; int nDestLen = Base64EncodeGetRequiredLength(sSource.GetLength()); CString str64; Base64Encode((const BYTE*)(LPCSTR)sSource, sSource.GetLength(),
str64.GetBuffer(nDestLen), &nDestLen); str64.ReleaseBuffer(nDestLen); cout<<(LPCSTR)str64; int nDecLen = Base64DecodeGetRequiredLength(nDestLen); CString strOrig; Base64Decode(str64, str64.GetLength(), (BYTE*)strOrig.GetBuffer(nDecLen),
&nDecLen); strOrig.ReleaseBuffer(nDecLen); cout<<(LPCSTR)strOrig;
HTML 生成
HTML 生成参考 (MSDN)CHtmlGen html; CWriteStreamOnCString stream; html.Initialize(&stream); html.html(); html.head(); html.title(_T("Sample Page")); html.headEnd(); html.body(_T("Gray")); html.WriteRaw(_T("Using ATL7!")); html.bodyEnd(); html.htmlEnd(); cout<<(LPCTSTR)stream.m_str;
HTTP 客户端
HTTP 客户端参考 (MSDN)class CHttpClientWithCookie : public CAtlHttpClient { template<> void OnSetCookie(LPCTSTR cookie) { std::cout<<"Got Cookie from server: "<<std::endl<<cookie<<std::endl; return; } }; { CHttpClientWithCookie client; //client.NavigateChunked() client.Navigate("http://www.microsoft.com"); const char* pszBody = (const char*)client.GetBody(); CString sRawHdr; DWORD dwLen; client.GetRawResponseHeader(0, &dwLen); client.GetRawResponseHeader((BYTE*)sRawHdr.GetBufferSetLength(dwLen), &dwLen); std::cout<<"Raw Response Header: "<<std::endl<<(LPCTSTR)sRawHdr; }还有许多其他杂项辅助类和更具体的类,用于 Web 应用程序/Web 服务开发。
调试
调试 (MSDN)
ISAPI 过滤器/扩展概述
ISAPI 是 Internet 服务器应用程序编程接口的缩写。ISAPI 编程分为过滤器和扩展。两者都是具有特定导出函数的 DLL。ISAPI 过滤器随 IIS 加载,并驻留在内存中,直到 HTTP Web 服务关闭。ISAPI 扩展按需加载,并为 Web 应用程序提供扩展功能。ISAPI 过滤器可用于检查/过滤 IIS 中的传入和传出 HTTP 请求(例如,实现自定义身份验证、加密、压缩、日志记录等)。ISAPI 扩展可用于开发动态网页,并按 URL 引用工作。
ATL7 中没有 ISAPI 过滤器的类。ATL Server 以 DLL 缓存、文件缓存、页面缓存、内存缓存、数据源缓存、线程池、通过基于 Web 或基于 Web 服务的接口进行远程管理、预定义的性能计数器、会话支持等形式为 ISAPI 扩展提供支持。正如您所见,这提供了相当多的功能/服务。此外,还可以轻松地将您自己的服务添加到现有服务中。
- ISAPI 服务器扩展和过滤器之间有什么区别 (MSDN)
- ISAPI 和 Web 应用程序体系结构 (MSDN)
- 设计 ISAPI 扩展应用程序 (MSDN)
- 设计 ISAPI 过滤器应用程序 (MSDN)
- ISAPI 过滤器示例 (MSDN)
- ISAPI 扩展示例 (MSDN)
- ATL7 ISAPIFilter 示例:将 URL 映射到参数化查询 (MSDN)
Web 应用程序
- ATL7 项目和 IIS (MSDN)
- 演练:ATL Server 教程 (MSDN) (强烈建议您学习本教程。)
- ATL7 ISAPI 扩展 DLL (MSDN)
- Web 应用程序 DLL (MSDN)
- 扩展管理服务 (MSDN)
- 事件序列 (MSDN)
ATL7 Web 应用程序是 ISAPI 扩展和处理程序 DLL 的组合(也可以将两者合并到一个 DLL 中),用于创建动态网页。ISAPI 扩展是一个导出三个函数的 DLL:HttpExtensionProc, GetExtensionVersion, TerminateExtension
。处理程序 DLL/Web 应用程序 DLL 还为 ATL 导出了三个预定义函数:InitializeAtlHandlers, GetAtlHandlerByName, UninitializeAtlHandlers
。
ATL7 Web 应用程序基于模板文件(stencil)构建。Stencil 是一个文本文件,通常带有 .srf 扩展名,位于 ISAPI 扩展的虚拟目录中,它可以包含静态 HTML 和动态/运行时可替换标签的混合内容。在 GetExtensionVersion
中,ATL 初始化它使用的堆、预定义的性能监视器、工作线程、线程池、DLL 缓存、页面缓存和模板缓存。TerminateExtension
执行相反的操作。
执行所有工作的核心函数是 HttpExtensionProc
。负责实现它的类是 CIsapiExtension
。CIsapiExtension::HttpExtensionProc
将传入的请求排队到 I/O 完成端口。工作线程(在线程池 CThreadPool
中运行)会检查此完成端口。所有这些工作线程所做的就是阻塞在上述完成端口上的 GetQueuedCompletionStatus
(请求是从 HttpExtensionProc
排队到该端口的)。默认情况下,池中启动 2 个线程(由 ATLS_DEFAULT_THREADSPERPROC
或 CThreadPool
实现的 IThreadPoolConfig
控制)。
因此,当任何一个工作线程获得机会并成功从端口中出队完成请求时,它们就会执行该请求。执行请求意味着处理模板文件,首先在页面缓存中查找整个处理后的页面(通过覆盖 CachePage()
方法并从派生的 CRequestHandlerT
类返回 TRUE 来控制)。如果找到,则将整个页面文件简单地传输给用户,一切就绪。否则,在模板缓存中查找模板文件。如果不在缓存中,则从磁盘读取 (.srf) 文件,解析处理程序 DLL 标签,并加载 DLL(如果尚未在 DLL 缓存中)。
调用处理程序的 DLL 入口点,即 InitializeAtlHandlers
,并通过 GetAtlHandlerByName
找到处理请求的处理程序类。然后,解析模板文件中的所有剩余可替换标签。渲染模板文件,即,对于所有代表方法的标签,通过使用属性时的 `tag_name` 定义的 `GetAttrReplacementMethodMap` 或派生的 CRequestHandlerT
类中的 REPLACEMENT_METHOD_MAP
来查找处理程序类中的方法地址。然后调用方法并生成动态内容以用于网页。生成的内容会发送回用户。最好在调试器中检查事件序列,并建议您也这样做。了解其内部工作原理很有意思。
要单步调试 ATL 代码,请在 HttpExtensionProc
处设置断点,然后单步调试直到 PostQueuedCompletionStatus
。另一个可以设置断点的地方是 GetQueuedCompletionStatus
的 while
循环。现在您可以按 F5 并按照整个步骤序列进行。
向导生成的代码(当您选择数据源缓存时,您也可以自己完成,但这可以作为示例)还会为每个线程创建自定义的 CIsapiWorker
派生类,用于线程级服务。这是为线程池中的每个线程创建的类。这允许您为每个线程添加自己的方法和数据成员。这意味着它不需要同步,并且您的请求处理程序类可以通过调用 IIsapiExtension::GetThreadWorker 来获取指向此线程级工作线程的指针(链接中也有一个使用它的示例)。
ATL Server 还提供多种方法来控制和配置您的 Web 应用程序。它为线程池、SRF 文件缓存和 Web 应用 DLL 缓存的远程控制提供了默认实现。您只需定义您想通过 HTML Web 界面或 Web 服务公开的任何服务的适当符号,限制给特定用户等,即可免费获得完整的实现!您可以按照 扩展管理服务 中描述的步骤进行操作,逐步完成!
扩展管理 XML Web 服务客户端
当您创建新项目时选择的服务:Blob 缓存、文件缓存、数据源缓存、浏览器功能、会话服务,以及框架提供的服务:DllCache、StencilCache、ThreadPoolConfig 和 AtlMemMgr,都通过 IServiceProvider::QueryService
公开,而 CIsapiExtension
派生类实现了该接口。您还可以通过 IIsapiExtension::AddService
在运行时添加您自己的任何服务,稍后通过 QueryService
查询它们。第一组服务的代码将添加到您的请求处理程序类中,并且会被注释掉。您所需要做的就是取消注释并根据您的需求应用它们。
此外,还有 预定义的 ISAPI 性能计数器。添加自己的性能计数器也很简单。在 ATL Server 教程的收集统计信息部分有一个简单的示例,演示了这一点以及添加自定义服务。
- ATL Server 示例 (MSDN)
- 使用 ATL Server 开发 Web 应用程序
- 开发高性能 Web 应用程序变得更加容易 (MSDN)
Web服务
- XML Web 服务基础 (MSDN)
- 演练:使用 ATL Server 创建 XML Web 服务 (MSDN)
- 演练:使用 C++ 访问 XML Web 服务 (MSDN)
- 使用 ATL Server 创建的 XML Web 服务 (MSDN)
- SOAP 示例 (MSDN)
- Web 服务描述语言 (WSDL) 详解 (MSDN)
- 简单对象访问协议 (SOAP) 1.1 (W3C)
- 通用描述、发现和集成 (UDDI) 规范
- Web 服务描述语言 (WSDL) (W3C)
- Web 服务规范 (MSDN)
ATL Web 服务支持也作为 ISAPI 扩展实现。您可以使用向导,也可以出于学习目的手动尝试。创建一个简单的 Win32 DLL 项目。添加 .def 文件以导出 3 个 ISAPI 扩展函数。然后,最简单的形式,用 ATL7 实现的 Web 服务可以如下所示:
//websvc.h #pragma once namespace ATL7WebService { [export] struct S { int n; double d; }; [uuid("86C903DD-4D9F-4928-A3B5-AE242EA86D7A")] __interface IWebSvc { HRESULT GetString([out,retval] BSTR* pVal); HRESULT GetData([out, retval] ATLSOAP_BLOB* pBlob); HRESULT GetStruct([out, retval] S* pVal); //etc }; [request_handler(name="Default", sdl="WSDL")] [soap_handler(name="WebSvc", namespace="urn:ATL7WebService")] class CWebSvc : public IWebSvc { public: [soap_method] HRESULT GetString(/*[out,retval]*/ BSTR* pVal) { CComBSTR strOut("Some String"); *pVal = strOut.Detach(); return S_OK; } [soap_method] HRESULT GetData(/*[out, retval]*/ ATLSOAP_BLOB* pBlob) { IAtlMemMgr* pMem = GetMemMgr(); pBlob->size = 1000; pBlob->data = (unsigned char*)pMem->Allocate(pBlob->size); memset(pBlob->data, 'a', pBlob->size); return S_OK; } [soap_method] HRESULT GetStruct(/*[out, retval]*/ S* pVal) { pVal->n = 10; pVal->d = 1.1; return S_OK; } //etc }; }
//websvc.cpp #define _WIN32_WINNT 0x0403 //need for attributes #define _ATL_ATTRIBUTES //not using COM for this simple project #define _ATL_NO_COM_SUPPORT #include <atlisapi.h> #include <atlsoap.h> #include "websvc.h" [ module(name="WebSvc", type="dll") ]; //don't need any .idl, .tlb, etc files generated [ emitidl(restricted) ]; CIsapiExtension<> theExtension; extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) { return theExtension.HttpExtensionProc(lpECB); } extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) { return theExtension.GetExtensionVersion(pVer); } extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) { return theExtension.TerminateExtension(dwFlags); }
您可以使用 VC++.NET 项目属性进行 Web 部署,并在应用程序映射中添加 .dll 扩展名,或者创建虚拟目录并在 IIS 下手动注册应用程序映射。之后,您可以在 Web 浏览器中以以下方式访问 Web 服务的 WSDL:https:///websvc/WebSvc.dll?Handler=WSDL
WebSvc.dll 包含您的 Web 服务的实现,WSDL 是您在 `request_handler` 中为 sdl 参数指定的编译器生成的处理程序。
现在,当您像前面部分中的 Web 应用程序一样再次访问它时,IIS 将调用 HttpExtensionProc
。它会将控制权委托给 CIsapiExtension
。CIsapiExtension
会将请求排队到 I/O 完成端口。一个工作线程将出队请求。然后加载处理程序 DLL (websvc.dll)。初始化处理程序。它的作用是获取您 Web 服务类公开的所有函数,以将关于它们的信息写入 WSDL。ATL 已经有了一个用于生成 Web 服务 WSDL 的模板。它可以在 `atlspriv.h` 中找到(const char * const s_szAtlsWSDLSrf)。您会看到它是一个简单的模板文本,适合模板处理(带有可替换标签)。因此,此字符串使用 CStencil::LoadFromString
加载,所有标签都会被解析和替换/渲染。
现在,此 WSDL 输出可用于生成您的 Web 服务的客户端。您可以使用 VS.NET 的内置支持添加 Web 服务,通过手动运行 `wsdl.exe` 来生成 .NET 客户端,或者运行 ATL7 提供的名为 `sproxy.exe` 的工具来生成代理客户端文件。例如:sproxy /out:client.h http://localhost/websvc/WebSvc.dll?Handler=WSDL
如果使用 sproxy.exe
,生成的文件将包含一个 C++ 类,该类 nicely 地包装了您的 Web 服务公开的方法。要使用它,只需将生成的文件(例如 client.h
)包含在您的项目中。
//client.cpp #include <iostream> #include "client.h" int main() { CoInitialize(NULL); { WebSvc::CWebSvc svc; CComBSTR str; if(svc.GetString(&str.m_str) == S_OK) { std::wcout<<str.m_str<<std::endl; ATLSOAP_BLOB blob; if(svc.GetData(&blob) == S_OK) { std::cout<<"blob of size: "<<blob.size<<std::endl; std::cout<<blob.data[0]<<blob.data1><<std::endl; IAtlMemMgr* pMem = svc.GetMemMgr(); pMem->Free(blob.data); WebSvc::S s; svc.GetStruct(&s); } } } CoUninitialize(); return 0; }
如果您想使用 Windows 集成身份验证或基本身份验证,可以使用 CNTLMAuthObject/CBasicAuthObject
(CSoapSocketClientT
特有)
CoInitialize(NULL); { //by default uses CSoapSocketClientT WebSvc::CWebSvc svc; //this code is specific to CSoapSocketClientT CAtlHttpClient& httpClient = svc.m_socket; CNTLMAuthObject authUser; httpClient.AddAuthObj(_T("NTLM"), &authUser); httpClient.NegotiateAuth(true); CComBSTR str; if(svc.GetString(&str.m_str) == S_OK) { //etc } } CoUninitialize();
如果您想使用 HTTPS 连接,可以查看 Secure SOAP 示例。
SecureSOAP 示例:实现安全的 SOAP 通信 (HTTPS) (MSDN)
此外,您还可以选择使用预定义的通信类,例如 CSoapSocketClientT
(默认)、CSoapWininetClient、CSoapMSXMLInetClient
WebSvc::CWebSvcT<CSoapWininetClient> svc; CComBSTR str; if(svc.GetString(&str.m_str) == S_OK) { //etc }
或者您自己的 SOAP 传输实现。本示例演示了如何创建使用不同传输方式的 SOAP 服务器和客户端:套接字、Microsoft 消息队列、文件系统以及自定义 HTTP 侦听器。
SOAPTransport 示例:通过套接字、MSMQ、文件系统和 HTTP 侦听器通信 SOAP 消息 (MSDN)