首先介绍一下相关背景。最近在测试一个程序时发现，在任务执行完成之后，从任务管理器上来看，内存并没有下降到理论值上。程序在启动完成之后会占用一定的内存，在执行任务的时候，会动态创建一些内存，用于存储任务的执行状态，比如扫描了哪些页面，在扫描过程中一些收发包的记录等等信息。这些中间信息在任务结束之后会被清理掉。任务结束之后，程序只会保存执行过的任务列表，从理论上讲，任务结束之后，程序此时所占内存应该与程序刚启动时占用内存接近，但是实际观察的结果就是任务结束之后，与刚启动之时内存占用差距在100M以上，这很明显不正常，当时我的第一反应是有内存泄露内存泄露排查既然有内存泄露，那么下一步就是开始排查，由于程序是采用MFC编写的，那么自然就得找MFC的内存泄露排查手段。根据网上找到的资料，MFC在DEBUG模式中可以很方便的集成内存泄露检查机制的。首先在 stdafx.h 的头文件中加入#define _CRTDBG_MAP_ALLO #include <crtdbg.h>再在程序退出的地方加入代码_CrtDumpMemoryLeaks();如果发生内存泄露的话，在调试运行结束之后，观察VS的输出情况可以看到如下内容Detected memory leaks! Dumping objects -> .\MatriXayTest.cpp(38) : {1301} normal block at 0x0000000005584D30, 40 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete.在输出中会提示有内存泄露，下面则是泄露的具体内容，MatriXayTest.cpp 是发生泄露的代码文件，括号中的38代表代码所在行数，大括号中1301代表这是程序的第1301次分配内存，或者说第1301次执行malloc操作，再往后就是内存泄露的地址，以及泄露的大小，这个地址是进程启动之后随机分配的地址，参考意义不大。下面一行表示，当前内存中的具体值，从值上来看应该是分配了内存但是没有初始化。根据这个线索，我们来排查，找到第38行所在位置int *p = new int[10]; _CrtDumpMemoryLeaks(); return nRetCode;内存泄露正是出现在new了10个int类型的数据，但是后面没有进行delete操作，正是这个操作导致了内存泄露。到此为止，检测工具也找到了，下面就是加上这段代码，运行发生泄露的程序，查看结果再漫长的等待任务执行完成并自动停止之后，我发现居然没有发现内存泄露！！！我又重复运行任务多次，发现结果是一样的，这个时候我开始怀疑是不是这个库不准，于是我在数据节点的类中添加构造函数，统计任务执行过程中创建了多少个节点，再在析构中统计析构了多少个节点，最终发现这两个数据完全相同。也就是说真的没有发生内存泄露。在这里我也疑惑了，难道是任务管理器有问题？带着这个疑问，我自己写了一段代码，在程序中不定时打印内存占用情况，结果发现虽然与任务管理器有差异，但是结果是类似的，释放之后内存并没有大幅度的下降。我带着疑问查找资料的过程的漫长过程中，发现任务管理器的显示内存占用居然降下去了，我统计了一下时间，应该是在任务结束之后的30分钟到40分钟之间。带着这个疑问，我又重新发起任务，在任务结束，并等待一定时间之后，内存占用果然降下去了。这里我得出结论 程序中执行delete操作之后，系统并不会真的立即回收操作，而是保留这个内存一定时间，以便后续该进程在需要分配时直接使用结论验证科学一般来说需要大胆假设，小心求证，既然上面根据现象做了一些猜想，下面就需要对这个猜想进行验证。首先来验证操作系统在程序调用delete之后不会真的执行delete操作。我使用下面的代码进行验证//定义一个占1M内存的结构 struct data{ char c[1024 * 1024]; } data* pa[1024] = {0}; for (int i = 0; i < 1024; i++) { pa[i] = new data; //这里执行一下数据清零操作，以便操作系统真正为程序分配内存 //有时候调用new或者malloc操作，操作系统只是保留对应地址，但是并未真正分配物理内存 //操作会等到进程真正开始操作这块内存或者进程需要分配的内存总量达到一个标准时才真正进行分配 memset(pa[i], 0x00, sizeof(data)); } printf("内存分配完毕，按任意键开始释放内存...\n"); getchar(); for (int i = 0; i < 1024; i++) { delete pa[i]; } printf("内存释放完毕,按任意键退出\n"); _CrtDumpMemoryLeaks(); char c = getchar();通过调试这段代码，在刚开始运行，没有执行到new操作的时候，进程占用内存在2M左右，运行到第一个循环结束，分配内存后，占用内存大概为1G，在执行完delete之后，内存并没有立马下降到初始的2M，而是稳定在150M左右，过一段时间之后，程序所占用内存会将到2M左右。接着对上面的代码做进一步修改，来测试内存使用时间长度与回收所需时间的长短的关系。这里仍然使用上面定义的结构体来做尝试data* pa[1024] = {0}; for (int i = 0; i < 1024; i++) { pa[i] = new data; memset(pa[i], 0x00, sizeof(data)); } printf("内存分配完毕，按任意键开始写数据到内存\n"); getchar(); //写入随机字符串 srand((unsigned) time(NULL)); DWORD dwStart = GetTickCount(); DWORD dwEnd = dwStart; printf("开始往目标内存中写入数据\n"); while ((dwEnd - dwStart) < 1 * 60 * 1000) //执行时间为1分钟 { for (int i = 0; i < 1024; i++) { for (int j = 0; j < 1024; j++) { int flag = rand() % 3; switch (flag) { case 1: { //生成大写字母 pa[i]->c[j] = (char)(rand() % 26) + 'A'; } break; case 2: { //生成小写字母 pa[i]->c[j] = (char)(rand() % 26) + 'a'; } break; case 3: { //生成数字 pa[i]->c[j] = (char)(rand() % 10) + '0'; } break; default: break; } } } dwEnd = GetTickCount(); } printf("数据写入完毕，按任意键开始释放内存...\n"); getchar(); for (int i = 0; i < 1024; i++) { delete pa[i]; } printf("内存释放完毕,按任意键退出\n"); _CrtDumpMemoryLeaks(); char c = getchar();后面就不放测试的结果了，我直接说结论，同一块内存使用时间越长，操作系统真正保留它的时间也会越长。短时间内差别可能不太明显，长时间运行，这个差别可以达到秒级甚至分。我记得当初上操作系统这门课程的时候，老师说过一句话：一个在过去使用时间越长的资源，在未来的时间内会再次使用到的概率也会越高，基于这一原理，操作会保留这块内存一段时间，如果程序在后面再次申请对应结构时，操作系统会直接将之前释放的内存拿来使用。为了验证这一现象，我来一个小的测试int *p1 = new int[1024]; memset(p, 0x00, sizeof(int) * 1024); delete[] p; int* p2= new int[1024];通过调试发现两次返回的地址相同，也就验证了之前说的内容总结最后来总结一下结论，有时候遇到delete内存后任务管理器或者其他工具显示内存占用未减少或者减少的量不对时，不一定是发生了内存泄露，也可能是操作系统的内存管理策略：程序调用delete后操作系统并没有真的立即回收对应内存，它只是暂时做一个标记，后续真的需要使用相应大小的内存时会直接将对应内存拿出来以供使用。而具体什么时候真正释放，应该是由操作系统进行宏观调控。我觉得这次暴露出来的问题还是自己基础知识掌握不扎实，如果当时我能早点回想起来当初上课时所讲的内容，可能也就不会有这次针对一个错误结论，花费这么大的精力来测试。当然这个世界上没有如果，我希望看到这篇博文的朋友，能少跟风学习新框架或者新语言，少被营销号带节奏，沉下心了，补充计算机基础知识，必将受益匪浅。
最近完成了一个使用VC++ 操作word生成扫描报告的功能，在这里将过程记录下来，开发环境为visual studio 2008导入接口首先在创建的MFC项目中引入word相关组件右键点击 项目 --> 添加 --> 新类,在弹出的对话框中选择Typelib中的MFC类。然后在弹出的对话框中选择文件，从文件中导入MSWORD.OLB组件。这个文件的路径一般在C:\Program Files (x86)\Microsoft Office\Office14 中，注意：最后一层可能不一定是Office14，这个看机器中安装的office 版本。选择之后会要求我们决定导入那些接口，为了方便我们导入所有接口。导入之后可以看到项目中省成本了很多代码文件，这些就是系统生成的操作Word的相关类。这里编译可能会报错，error C2786: “BOOL (HDC,int,int,int,int)”: __uuidof 的操作数无效解决方法：修改对应头文件#import "C:\\Program Files\\Microsoft Office\\Office14\\MSWORD.OLB" no_namespace为：#import "C:\\Program Files\\Microsoft Office\\Office14\\MSWORD.OLB" no_namespace raw_interfaces_only \ rename("FindText","_FindText") \ rename("Rectangle","_Rectangle") \ rename("ExitWindows","_ExitWindows")再次编译，错误消失常见接口介绍要了解一些常见的类，我们首先需要明白这些接口的层次结构：Application(WORD 为例，只列出一部分） Documents（所有的文档） Document（一个文档） ...... Templates（所有模板） Template（一个模板） ...... Windows（所有窗口） Window Selection View Selection（编辑对象） Font Style Range这些组件其实是采用远程调用的方式调用word进程来完成相关操作。Application：相当于一个word进程，每次操作之前都需要一个application对象，这个对象用于创建一个word进程。Documents：相当于word中打开的所有文档，如果用过word编辑多个文件，那么这个概念应该很好理解Templates：是一个模板对象，至于word模板，不了解的请自行百度Windows：word进程中的窗口Selection：编辑对象。也就是我们要写入word文档中的内容。一般包括文本、样式、图形等等对象。回忆一下我们手动编写word的情景，其实使用这些接口是很简单的。我们在使用word编辑的时候首先会打开word程序，这里对应在代码里面就是创建一个Application对象。然后我们会用word程序打开一个文档或者新建一个文档。这里对应着创建Documents对象并从中引用一个Document对象表示一个具体的文档。当然这个Document对象可以是新建的也可以是打开一个现有的。接着就是进行相关操作了，比如插入图片、插入表格、编写段落文本等等了。这些都对应着创建类似于Font、Style、TypeText对象，然后将这些对象进行添加的操作了。说了这么多。这些接口这么多，我怎么知道哪个接口对应哪个对象呢，而且这些参数怎么传递呢？其实这个问题很好解决。我们可以手工进行相关操作，然后用宏记录下来，最后我们再将宏中的VB代码转化为VC代码即可。相关操作为了方便在项目中使用，这里创建一个类用于封装Word的相关操作class CCreateWordReport { private: CApplication m_wdApp; CDocuments m_wdDocs; CDocument0 m_wdDoc; CSelection m_wdSel; CRange m_wdRange; CnlineShapes m_wdInlineShapes; CnlineShape m_wdInlineShape; public: CCreateWordReport(); virtual ~CCreateWordReport(); public: //操作 //**********************创建新文档******************************************* BOOL CreateApp(); //创建一个新的WORD应用程序 BOOL CreateDocuments(); //创建一个新的Word文档集合 BOOL CreateDocument(); //创建一个新的Word文档 BOOL Create(); //创建新的WORD应用程序并创建一个新的文档 void ShowApp(); //显示WORD文档 void HideApp(); //隐藏word文档 //**********************打开文档********************************************* BOOL OpenDocument(CString fileName);//打开已经存在的文档。 BOOL Open(CString fileName); //创建新的WORD应用程序并打开一个已经存在的文档。 BOOL SetActiveDocument(short i); //设置当前激活的文档。 //**********************保存文档********************************************* BOOL SaveDocument(); //文档是以打开形式，保存。 BOOL SaveDocumentAs(CString fileName);//文档以创建形式，保存。 BOOL CloseDocument(); void CloseApp(); //**********************文本书写操作***************************************** void WriteText(CString szText); //当前光标处写文本 void WriteNewLineText(CString szText, int nLineCount = 1); //换N行写字 void WriteEndLine(CString szText); //文档结尾处写文本 void WholeStory(); //全选文档内容 void Copy(); //复制文本内容到剪贴板 void InsertFile(CString fileName); //将本地的文件全部内容写入到当前文档的光标处。 void InsertTable(int nRow, int nColumn, CTable0& wdTable); //**********************图片插入操作***************************************** void InsertShapes(CString fileName);//在当前光标的位置插入图片 //**********************超链接插入操作***************************************** void InsertHyperlink(CString fileLink);//超级链接地址，可以是相对路径。 //***********************表格操作表格操作********************************** BOOL InsertTableToMarkBook(const CString csMarkName, int nRow, int nColumn, CTable0& wdTable); //表格行数与列数 BOOL WriteDataToTable(CTable0& wdTable, int nRow, int nColumn, const CString &csData); //往表格中写入输入 };BOOL CCreateWordReport::CreateApp() { if (FALSE == m_wdApp.CreateDispatch("word.application")) { AfxMessageBox("Application创建失败，请确保安装了word 2000或以上版本!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::CreateDocuments() { if (FALSE == CreateApp()) { return FALSE; } m_wdDocs = m_wdApp.get_Documents(); if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents创建失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::CreateDocument() { if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents为空!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant varTrue(short(1),VT_BOOL),vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); CComVariant Template(_T("")); //没有使用WORD的文档模板 CComVariant NewTemplate(false),DocumentType(0),Visible; m_wdDocs.Add(&Template,&NewTemplate,&DocumentType,&Visible); //得到document变量 m_wdDoc = m_wdApp.get_ActiveDocument(); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::Create() { if (FALSE == CreateDocuments()) { return FALSE; } return CreateDocument(); } BOOL CCreateWordReport::OpenDocument(CString fileName) { if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents为空!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR), vZ((short)0); COleVariant vFileName(_T(fileName)); //得到document变量 m_wdDoc = m_wdDocs.Open( vFileName, // FileName vTrue, // Confirm Conversion. vFalse, // ReadOnly. vFalse, // AddToRecentFiles. vOptional, // PasswordDocument. vOptional, // PasswordTemplate. vOptional, // Revert. vOptional, // WritePasswordDocument. vOptional, // WritePasswordTemplate. vOptional, // Format. // Last argument for Word 97 vOptional, // Encoding // New for Word 2000/2002 vOptional, // Visible /*如下4个是word2003需要的参数。本版本是word2000。*/ vOptional, // OpenAndRepair vZ, // DocumentDirection wdDocumentDirection LeftToRight vOptional, // NoEncodingDialog vOptional ); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::Open(CString fileName) { if (FALSE == CreateDocuments()) { return FALSE; } return OpenDocument(fileName); } BOOL CCreateWordReport::SetActiveDocument(short i) { COleVariant vIndex(_T(i)),vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.AttachDispatch(m_wdDocs.Item(vIndex)); m_wdDoc.Activate(); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } // HideApp(); return TRUE; } BOOL CCreateWordReport::SaveDocument() { if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } m_wdDoc.Save(); return TRUE; } BOOL CCreateWordReport::SaveDocumentAs(CString fileName) { if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant covOptional((long)DISP_E_PARAMNOTFOUND,VT_ERROR); COleVariant varZero((short)0); COleVariant varTrue(short(1),VT_BOOL); COleVariant varFalse(short(0),VT_BOOL); COleVariant vFileName(_T(fileName)); m_wdDoc.SaveAs( vFileName, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional ); return TRUE; } BOOL CCreateWordReport::CloseDocument() { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.Close(vFalse, // SaveChanges. vTrue, // OriginalFormat. vFalse // RouteDocument. ); m_wdDoc.AttachDispatch(m_wdApp.get_ActiveDocument()); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } void CCreateWordReport::CloseApp() { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.Save(); m_wdApp.Quit(vFalse, // SaveChanges. vTrue, // OriginalFormat. vFalse // RouteDocument. ); //释放内存申请资源 m_wdInlineShape.ReleaseDispatch(); m_wdInlineShapes.ReleaseDispatch(); //m_wdTb.ReleaseDispatch(); m_wdRange.ReleaseDispatch(); m_wdSel.ReleaseDispatch(); //m_wdFt.ReleaseDispatch(); m_wdDoc.ReleaseDispatch(); m_wdDocs.ReleaseDispatch(); m_wdApp.ReleaseDispatch(); } void CCreateWordReport::WriteText(CString szText) { m_wdSel.TypeText(szText); } void CCreateWordReport::WriteNewLineText(CString szText, int nLineCount /* = 1 */) { int i; if (nLineCount <= 0) { nLineCount = 0; } for (i = 0; i < nLineCount; i++) { m_wdSel.TypeParagraph(); } WriteText(szText); } void CCreateWordReport::WriteEndLine(CString szText) { m_wdRange.InsertAfter(szText); } void CCreateWordReport::WholeStory() { m_wdRange.WholeStory(); } void CCreateWordReport::Copy() { m_wdRange.CopyAsPicture(); } void CCreateWordReport::InsertFile(CString fileName) { COleVariant vFileName(fileName), vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR), vNull(_T("")); /* void InsertFile(LPCTSTR FileName, VARIANT* Range, VARIANT* ConfirmConversions, VARIANT* Link, VARIANT* Attachment); */ m_wdSel.InsertFile( fileName, vNull, vFalse, vFalse, vFalse ); } void CCreateWordReport::InsertShapes(CString fileName) { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdInlineShapes=m_wdSel.get_InlineShapes(); m_wdInlineShape=m_wdInlineShapes.AddPicture(fileName,vFalse,vTrue,vOptional); } void CCreateWordReport::InsertHyperlink(CString fileLink) { COleVariant vAddress(_T(fileLink)),vSubAddress(_T("")); CRange aRange = m_wdSel.get_Range(); CHyperlinks vHyperlinks(aRange.get_Hyperlinks()); vHyperlinks.Add( aRange, //Object，必需。转换为超链接的文本或图形。 vAddress, //Variant 类型，可选。指定的链接的地址。此地址可以是电子邮件地址、Internet 地址或文件名。请注意，Microsoft Word 不检查该地址的正确性。 vSubAddress, //Variant 类型，可选。目标文件内的位置名，如书签、已命名的区域或幻灯片编号。 vAddress, //Variant 类型，可选。当鼠标指针放在指定的超链接上时显示的可用作“屏幕提示”的文本。默认值为 Address。 vAddress, //Variant 类型，可选。指定的超链接的显示文本。此参数的值将取代由 Anchor 指定的文本或图形。 vSubAddress //Variant 类型，可选。要在其中打开指定的超链接的框架或窗口的名字。 ); aRange.ReleaseDispatch(); vHyperlinks.ReleaseDispatch(); }这样我们就封装好了一些基本的操作，其实这些操作都是我自己根据网上的资料以及VB宏转化而来得到的代码。特殊操作在这里主要介绍一些比较骚的操作，这也是这篇文章主要有用的内容，前面基本操作网上都有源代码直接拿来用就OK了，这里的骚操作是我在项目中使用的主要操作，应该有应用价值。先请各位仔细想想，如果我们要根据前面的代码，从0开始完全用代码生成一个完整的报表是不是很累，而且一般报表都会包含一些通用的废话，这些话基本不会变化。如果将这些写到代码里面，如果后面这些话变了，我们就要修改并重新编译，是不是很麻烦。所以这里介绍的第一个操作就是利用模板和书签在合适的位置插入内容。书签的使用首先我们在Word中的适当位置创建一个标签，至于如何创建标签，请自行百度。然后在代码中的思路就是在文档中查找我们的标签，再获取光标的位置，最后就是在该位置处添加相应的内容了，这里我们举一个在光标位置插入文本的例子:void CCreateWordReport::WriteTextToBookMark(const CString& csMarkName, const CString& szText) { CBookmarks bks = m_wdDoc.get_Bookmarks(); //获取文档中的所有书签 CBookmark0 bk; COleVariant bk_name(csMarkName); bk = bks.Item(&bk_name); //查询对应名称的书签 CRange hRange = bk.get_Range(); //获取书签位置 if (hRange != NULL) { hRange.put_Text(szText); //在该位置处插入文本 } //最后不要忘记清理相关资源 hRange.ReleaseDispatch(); bk.ReleaseDispatch(); bks.ReleaseDispatch(); }表格的使用在word报表中表格应该是一个重头戏，表格中常用的接口如下:CTables0: 表格集合CTable0: 某个具体的表格，一般通过CTables来创建CTableCColumn: 表格列对象CRow：表格行对象CCel：表格单元格对象创建表格一般的操作如下:void CCreateWordReport::InsertTable(int nRow, int nColumn, CTable0& wdTable) { VARIANT vtDefault; COleVariant vtAuto; vtDefault.vt = VT_INT; vtDefault.intVal = 1; vtAuto.vt = VT_INT; vtAuto.intVal = 0; CTables0 wordtables = m_wdDoc.get_Tables(); wdTable = wordtables.Add(m_wdSel.get_Range(), nRow, nColumn, &vtDefault, &vtAuto); wordtables.ReleaseDispatch(); }往表格中写入内容的操作如下:BOOL CCreateWordReport::WriteDataToTable(CTable0& wdTable, int nRow, int nColumn, const CString &csData) { CCell cell = wdTable.Cell(nRow, nColumn); cell.Select(); //将光标移动到单元格 m_wdSel.TypeText(csData); cell.ReleaseDispatch(); return TRUE; }合并单元格的操作如下:CTable0 wdTable; InsertTable(5, 3, wdTable); //创建一个5行3列的表格 CCell cell = wdTable.Cell(1, 1); //获得第一行第一列的单元格 //设置第二列列宽 CColumns0 columns = wdTable.get_Columns(); CColumn col; col.AttachDispatch(columns.Item(2)); col.SetWidth(40, 1); cell.Merge(wdTable.Cell(5, 1)); //合并单元格，一直合并到第5行的第1列。 cell.SetWidth(30, 1); cell.ReleaseDispatch();合并单元格用的是Merge函数，该函数的参数是一个单元格对象，表示合并结束的单元格。这里合并类似于我们画矩形时提供的左上角坐标和右下角坐标移动光标跳出表格当时由于需要连续的生成多个表格，当时我将前一个表格的数据填完，光标位于最后一个单元格里面，这个时候如果再插入的时候会在这个单元格里面插入表格，这个时候需要我手动向下移动光标，让光标移除到表格外。移动光标的代码如下:m_wdSel.MoveDown(&COleVariant((short)wdLine), &COleVariant((short)1), &COleVariant((short)wdNULL)); 这里wdLine 是word相关接口定义的，表示希望以何种单位来移动，这里我是以行为单位。后面的1表示移动1行。但是我发现在面临换页的时候一次移动根本移动不出来，这个时候我又添加了一行这样的代码移动两行。但是问题又出现了，这一系列表格后面跟着另一个大标题，多移动几次之后可能会造成它移动到大标题的位置，而破坏我原来定义的模板，这个时候该怎么办呢？我采取的办法是，判断当前光标是否在表格中，如果是则移动一行，知道出了表格。这里的代码如下://移动光标，直到跳出表格外 while (TRUE) { m_wdSel.MoveDown(&COleVariant((short)wdLine), &COleVariant((short)1), &COleVariant((short)wdNULL)); m_wdSel.Collapse(&COleVariant((short)wdCollapseStart)); if (!m_wdSel.get_Information((long)wdWithInTable).boolVal) { break; } }样式的使用在使用样式的时候当然也可以用代码来定义，但是我们可以采取另一种方式，我们可以事先在模板文件中创建一系列样式，然后在需要的时候直接定义段落或者文本的样式即可m_wdSel.put_Style(COleVariant("二级标题")); //在当前光标处的样式定义为二级标题样式，这里的二级标题样式是我们在word中事先定义好的 m_wdSel.TypeText(csTitle); //在当前位置输出文本 m_wdSel.TypeParagraph(); //插入段落，这里主要为了换行，这个时候光标也会跟着动 m_wdSel.put_Style(COleVariant("正文")); //定义此处样式为正文样式 m_wdSel.TypeText(csText;插入图表我自己尝试用word生成的图表样式还可以，但是用代码插入的时候，样式就特别丑，这里没有办法，我采用GDI+绘制了一个饼图，然后将图片插入word中。BOOL CCreateWordReport::DrawVulInforPic(const CString& csMarkName, int nVulCnt, int nVulCris, int nHigh, int nMid, int nLow, int nPossible) { CBookmarks bks = m_wdDoc.get_Bookmarks(); COleVariant bk_name(csMarkName); CBookmark0 bk = bks.Item(&bk_name); bk.Select(); CnlineShapes isps = m_wdSel.get_InlineShapes(); COleVariant vFalse((short)FALSE); COleVariant vNull(""); COleVariant vOptional((long)DISP_E_PARAMNOTFOUND,VT_ERROR); //创建一个与桌面环境兼容的内存DC HWND hWnd = GetDesktopWindow(); HDC hDc = GetDC(hWnd); HDC hMemDc = CreateCompatibleDC(hDc); HBITMAP hMemBmp = CreateCompatibleBitmap(hDc, PICTURE_WIDTH + GLOBAL_MARGIN, PICTURE_LENGTH + 2 * GLOBAL_MARGIN + LENGED_BORDER_LENGTH); SelectObject(hMemDc, hMemBmp); //绘制并保存图表 DrawPie(hMemDc, nVulCnt, nVulCris, nHigh, nMid, nLow, nPossible); COleVariant vTrue((short)TRUE); CnlineShape isp=isps.AddPicture("D:\\Program\\WordReport\\WordReport\\test.png",vFalse,vTrue,vOptional); //以图片的方式插入图表 //设置图片的大小 isp.put_Height(141); isp.put_Width(423); bks.ReleaseDispatch(); bk.ReleaseDispatch(); isps.ReleaseDispatch(); isp.ReleaseDispatch(); DeleteObject(hMemDc); DeleteDC(hMemDc); ReleaseDC(hWnd, hDc); return TRUE; }最后，各个接口的参数可以参考下面的链接:.net Word office组件接口文档
最近公司产品中自定义浏览器比较老，打开一些支持h5 的站莫名报错，而且经常弹框。已经到了令人无法忍受的地步了，于是我想到了将内核由之前的IE 升级到Chromium。之前想到的是使用cef来做，而且网上的资源和教程也很多，后来在自己尝试的过程中发现使用cef时程序会莫名其妙的崩溃，特别是在关闭对话框的时候。我在网上找了一堆资料，尝试了各种版本未果，这个方案也就放弃了。后来又搜到了wke和miniblink，对比二者官方的文档和demo，我决定使用miniblink，毕竟我直接搜索wke browser 出来的都是miniblink，只有搜索wke github 才会有真正的wke，而且wke似乎没有api文档，最后miniblink是国人写的，文档都是中文而且又有专门的qq交流群，有问题可以咨询一下。什么是miniblinkminiblink 是由国内大神 龙泉寺扫地僧 针对chromium内核进行裁剪去掉了所有多余的部件，只保留最基本的排版引擎blink，而产生的一款号称全球小巧的浏览器内核项目，目前miniblink 保持了10M左右的极简大小，相比CEF 动辄几百M的大小确实小巧许多。而且能很好的支持H5等一些列新标准，同时它内嵌了Nodejs 支持electron。而且也支持各种语言调用。官方的地址如下为 https://weolar.github.io/miniblink/index.html使用miniblink说了这么多那么该怎么用呢？从官方的介绍来看，我们可以使用VS的向导程序生成一个普通的win32 窗口程序，然后生成的这些代码中将函数InitInstance 中的代码全部删除加上这么5句话wkeSetWkeDllPath(L"E:\\mycode\\miniblink49\\trunk\\out\\Release_vc6\\node.dll"); wkeInitialize(); wkeWebView window = wkeCreateWebWindow(WKE_WINDOW_TYPE_POPUP, NULL, 0, 0, 1080, 680); wkeLoadURL(window, "qq.com"); wkeShowWindow(window, TRUE);当然，使用这些函数需要下载它的SDK开发包，然后在对应位置包含wke.h。这些代码会生成一个窗口程序，具体的请敢兴趣的朋友自己去实践看看效果。或者编译运行一下它的demo程序。在对话框中使用现在我想在对话框中使用，那么该怎么办呢。首先也是先用MFC的向导生成一个对话框并编辑资源文件。最后我的对话框大概长成这样我会将按钮下面部分全部作为浏览器页面。我们在程序APP类的InitInstance函数 中初始化miniblink库,并在对话框被关闭后直接卸载miniblink的相关资源wkeSetWkeDllPath(L"node.dll"); wkeInitialize(); CWebBrowserDlg dlg = CWebBrowserDlg(); m_pMainWnd = dlg; INT_PTR nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: 在此放置处理何时用 // “确定”来关闭对话框的代码 } else if (nResponse == IDCANCEL) { // TODO: 在此放置处理何时用 // “取消”来关闭对话框的代码 } // 由于对话框已关闭，所以将返回 FALSE 以便退出应用程序， // 而不是启动应用程序的消息泵。 wkeFinalize();然后在主对话框类中新增一个成员变量用来保存miniblink的web视图的句柄wkeWebView m_web;我们在对话框的OnInitDialog函数中创建这么一个视图，用来加载百度的首页面GetClientRect(&rtClient); rtClient.top += 24; m_web = wkeCreateWebWindow(WKE_WINDOW_TYPE_CONTROL, *this, rtClient.left, rtClient.top, rtClient.right - rtClient.left, rtClient.bottom - rtClient.top); wkeLoadURL(m_web, "https://www.baidu.com"); wkeShowWindow(m_web, TRUE);至此我们已经能够生成一个简单的浏览器程序似乎到这已经差不多该结束了，但是现在我遇到了在整个程序完成期间最大的问题，那就是web页面无法响应键盘消息，我尝试过改成窗口程序，发现改了之后能正常运行，但是我要的是对话框啊。这么改只能证明这个库是没问题的。后来我在群里面发出了这样的疑问，有朋友告诉我说应该是wkeWebView没有接受到键盘消息，于是我打算处理主对话框的WM_KEYDOWN 和WM_KEYUP 以及WM_CHAR消息，根据官方的文档，应该是只需要拦截对话框的这三个消息，然后使用函数wkeFireKeyUpEvent、wkeFireKeyDownEvent、wkeFireKeyPressEvent函数分别向wkeWebView发送键盘消息就可以了.于是我在对应的处理函数中添加了相关代码//OnChar unsigned int flags = 0; if (nFlags & KF_REPEAT) flags |= WKE_REPEAT; if (nFlags & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyPressEvent(m_web, nChar, flags, false);//OnKeyUp unsigned int flags = 0; if (nFlags & KF_REPEAT) flags |= WKE_REPEAT; if (nFlags & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyUpEvent(m_web, virtualKeyCode, flags, false);//OnKeyDown unsigned int flags = 0; if (nFlags & KF_REPEAT) flags |= WKE_REPEAT; if (nFlags & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyDownEvent(m_web, virtualKeyCode, flags, false);但是这么干，我通过调试发现它好像并没有进入到这些函数里面来，也就是说键盘消息不是由主对话框来处理的。那么现在只能在wkeWebView 对应的窗口中来处理了。那么怎么捕获这个窗口的消息呢，miniblink提供了函数wkeGetHostHWND 来根据视图的句柄获取对应窗口的句柄，那么现在的思路就是这样的：首先获取对应的窗口句柄然后通过SetWindowLong来修改窗口的窗口过程，然后在窗口过程中处理这些消息就行了。根据这个思路整理一下代码//在创建wkeWebView 之后来hook窗口过程 HWND hWnd = wkeGetHostHWND(m_web); g_OldProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc); //为了能够在全局函数中使用对话框类的东西，我们为窗口绑定一个对话框类的指针 SetWindowLong(hWnd, GWL_USERDATA, this);接着在MyWndProc中处理对应的消息事件LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWebBrowserDlg* pDlg = (CWebBrowserDlg*)GetWindowLong(hWnd, GWL_USERDATA); if (NULL == pDlg) { return CallWindowProc(g_OldProc, hWnd, uMsg, wParam, lParam); } switch (uMsg) { case WM_KEYUP: { unsigned int virtualKeyCode = wParam; unsigned int flags = 0; if (HIWORD(lParam) & KF_REPEAT) flags |= WKE_REPEAT; if (HIWORD(lParam) & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyDownEvent(pDlg->m_web, virtualKeyCode, flags, false); } break; case WM_KEYDOWN: { unsigned int virtualKeyCode = wParam; unsigned int flags = 0; if (HIWORD(lParam) & KF_REPEAT) flags |= WKE_REPEAT; if (HIWORD(lParam) & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyUpEvent(pDlg->m_web, virtualKeyCode, flags, false); } break; case WM_CHAR: { unsigned int charCode = wParam; unsigned int flags = 0; if (HIWORD(lParam) & KF_REPEAT) flags |= WKE_REPEAT; if (HIWORD(lParam) & KF_EXTENDED) flags |= WKE_EXTENDED; wkeFireKeyPressEvent(pDlg->m_web, charCode, flags, false); } break; default: return CallWindowProc(g_OldProc, hWnd, uMsg, wParam, lParam); } return 0; }这样做之后我发现它虽然能够截取到这些消息并执行它，但是在调用wkeFireKeyPressEvent等函数之后仍然无法响应键盘消息。难道是wkeCreateWebWindow 创建出来的窗口不能做子窗口？带着这个疑问我根据官方文档尝试了一下使用wkeCreateWebView ，然后将它绑定到对应的窗口上，然后这个整体作为子窗口的方式。代码太长了，我就不放出来了，有兴趣的可以翻到本文尾部，我将这个demo项目放到的GitHub上。结果还是不行。这些函数仍然进不来。真的郁闷，难道要换方案？我这个时候已经开始准备换方案了，在编译wke 的时候心情极度烦躁，我在之前的程序上不停的敲击键盘，就听见“等~等~等~”。我靠！这不是想从模态对话框上切换回主页面时的那个声音吗？会不会是因为模态对话框的关系？这个时候我瞬间来了灵感。那就换吧，主要改一下APP类中相关代码，吧模态改成非模态的就行CWebBrowserDlg *dlg = new CWebBrowserDlg(); dlg->Create(IDD_WEBBROWSER_DIALOG); m_pMainWnd = dlg; INT_PTR nResponse = dlg->ShowWindow(SW_SHOW); if (nResponse == IDOK) { // TODO: 在此放置处理何时用 // “确定”来关闭对话框的代码 } else if (nResponse == IDCANCEL) { // TODO: 在此放置处理何时用 // “取消”来关闭对话框的代码 } // 由于对话框已关闭，所以将返回 FALSE 以便退出应用程序， // 而不是启动应用程序的消息泵。 //由于是非模态对话框，所以这里需要自己写消息环 MSG msg = { 0 }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessageW(&msg); } delete dlg;卧槽，居然成功了，能正常相应了！为什么模态就不行呢，后来我在复盘的时候想到，应该是wkeWebView的窗口并没有做成那种严格意义上的子窗口，它是一个独立的，所以模态对话框把消息给拦截了不让传到其他的窗口导致的这个问题。这个也算是成功了。这个时候问题又来了，程序关不掉了，虽然说窗口是关了，但是程序并没有退出，后来调试发现，消息环没有退出。这个时候我想到应该是关闭时调用的是EndDialog。但是此时已经改成非模态了，需要最后调用DestroyWindow，那么这个地方就得去对话框的OnClose消息中改。void CWebBrowserDlg::OnClose() { // TODO: 在此添加消息处理程序代码和/或调用默认值 DestroyWindow(); //CDialog::OnClose(); }好了，这个时候基本已经完成了。就剩下一些按钮事件处理了。按钮事件的处理这里直接贴代码吧，基本只有几行，很容易看懂的void CWebBrowserDlg::OnBnClickedBtnBack() { // TODO: 在此添加控件通知处理程序代码 if (wkeCanGoBack(m_web)) { wkeGoBack(m_web); } } void CWebBrowserDlg::OnBnClickedBtnForward() { // TODO: 在此添加控件通知处理程序代码 if (wkeCanGoForward(m_web)) { wkeGoForward(m_web); } } void CWebBrowserDlg::OnBnClickedBtnStop() { // TODO: 在此添加控件通知处理程序代码 wkeStopLoading(m_web); } void CWebBrowserDlg::OnBnClickedBtnRefresh() { // TODO: 在此添加控件通知处理程序代码 wkeReload(m_web); } void CWebBrowserDlg::OnBnClickedBtnGo() { // TODO: 在此添加控件通知处理程序代码 CString csurl; GetDlgItem(IDC_EDIT_URL)->GetWindowText(csurl); wkeLoadURLW(m_web, csurl); } //设置代理 void CWebBrowserDlg::OnBnClickedBtnProxy() { CDlgProxySet dlgProxySet; dlgProxySet.DoModal(); wkeProxy proxy; proxy.type = WKE_PROXY_HTTP; USES_CONVERSION; strcpy_s(proxy.hostname, sizeof(proxy.hostname), T2A(dlgProxySet.csIP)); proxy.port = dlgProxySet.m_port; wkeSetProxy(&proxy); // TODO: 在此添加控件通知处理程序代码 }wkeView 的回调函数现在主体功能已经完成了，要跟浏览器类似，需要处理这样几个东西。第一个是url栏中的内容会根据当前主页面的url做调整，特别是针对302、301 跳转的情况。第二个是窗口的标题应该改为页面的标题；第三个是在某些页面中超链接用的是_blank，时应该能正常打开新窗口。为了实现这些目标，我们需要处理一些wkeView的事件,我们创建了wkeWebView 之后直接绑定这些事件wkeOnTitleChanged(m_web, wkeOnTitleChangedCallBack, this); //最后一个参数是传递用户数据，这里我们传递this指针进去 wkeOnURLChanged(m_web, wkeOnURLChangedCallBack, this); wkeOnNavigation(m_web, wkeOnNavigationCallBack, this); wkeOnCreateView(m_web, onBrowserCreateView, this);// 页面标题更改时调用此回调 void _cdecl wkeOnTitleChangedCallBack(wkeWebView webView, void* param, const wkeString title) { CWebBrowserDlg *pDlg = (CWebBrowserDlg*)param; if (NULL != pDlg) { pDlg->SetWindowText(wkeGetStringW(title)); } } //url变更时调用此回调 void _cdecl wkeOnURLChangedCallBack(wkeWebView webView, void* param, const wkeString url) { CWebBrowserDlg *pDlg = (CWebBrowserDlg*)param; if (NULL != pDlg) { pDlg->GetDlgItem(IDC_EDIT_URL)->SetWindowTextW(wkeGetStringW(url)); } } //网页开始浏览将触发回调, 这里主要是为了它能打开一些本地的程序 bool _cdecl wkeOnNavigationCallBack(wkeWebView webView, void* param, wkeNavigationType navigationType, const wkeString url) { const wchar_t* urlStr = wkeGetStringW(url); if (wcsstr(urlStr, L"exec://") == urlStr) { PROCESS_INFORMATION processInfo = { 0 }; STARTUPINFOW startupInfo = { 0 }; startupInfo.cb = sizeof(startupInfo); BOOL succeeded = CreateProcessW(NULL, (LPWSTR)urlStr + 7, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo); if (succeeded) { CloseHandle(processInfo.hProcess); CloseHandle(processInfo.hThread); } return false; } return true; } //网页点击a标签创建新窗口时将触发回调 wkeWebView _cdecl onBrowserCreateView(wkeWebView webView, void* param, wkeNavigationType navType, const wkeString urlStr, const wkeWindowFeatures* features) { const wchar_t* url = wkeGetStringW(urlStr); wkeWebView newWindow = wkeCreateWebWindow(WKE_WINDOW_TYPE_POPUP, NULL, features->x, features->y, features->width, features->height); wkeShowWindow(newWindow, true); return newWindow; }至此这个浏览器的demo就完成了。最后贴上对应的demo项目地址: https://github.com/aMonst/WebBrowserPS:最近有一位朋友发邮件告诉我说，wkeWebView 不能响应键盘消息与对话框是模态还是非模态无关，主要是要处理wkeWebView的WM_GETDLGCODE 消息，那位朋友给出的代码如下:switch(uMsg) { case WM_GETDLGCODE: return DLGC_WANTARROWS | DLGC_WANTALLKEYS | DLGC_WANTCHARS; }我试了一下，发现确实是这样，相比较我上面提出的改为非模态的方式来说，还是用模态对话框方便、毕竟MFC对话框程序本来就是非模态的。所以这里我将代码做了一下修改。并同步到了GitHub上。最后再次感谢那位发邮件告诉的朋友。。。。。参考资料miniblink API文档官方DEMO
title: 多结果集IMultipleResult接口tags: [OLEDB, 数据库编程, VC++, 数据库]date: 2018-03-16 21:04:31categories: windows 数据库编程keywords: OLEDB, 数据库编程, VC++, 数据库, 结果集, 多结果集在某些任务中，需要执行多条sql语句，这样一次会返回多个结果集，在应用程序就需要处理多个结果集，在OLEDB中支持多结果集的接口是IMultipleResult。查询数据源是否支持多结果集并不是所有数据源都支持多结果集，可以通过查询数据源对象的DBPROPSET_DATASOURCEINFO属性集中的DBPROP_MULTIPLERESULTS属性来确定，该值是一个按位设置的4字节的联合值。它可取的值有下面几个：DBPROPVAL_MR_SUPPORITED：支持多结果集DBPROPVAL_MR_SONCURRENT:支持多结果集，并支持同时打开多个返回的结果集（如果它不支持同时打开多个结果集的话，在打开下一个结果集之前需要关闭已经打开的结果集）DBPROPVAL_MR_NOTSUPPORTED: 不支持多结果集这个属性可以通过接口IDBProperties接口的GetProperties方法来获取,该函数的原型如下：HRESULT GetProperties ( ULONG cPropertyIDSets, const DBPROPIDSET rgPropertyIDSets[], ULONG *pcPropertySets, DBPROPSET **prgPropertySets);它的用法与之前的SetProperties十分类似第一个参数是我们传入DBPROPIDSET数组元素的个数，第二个参数是一个DBPROPIDSET结构的参数，该结构的原型如下：typedef struct tagDBPROPIDSET { DBPROPID * rgPropertyIDs; ULONG cPropertyIDs; GUID guidPropertySet; } DBPROPIDSET;该结构与之前遇到的DBPROPSET类似，第一个参数是一个DBPROPID结构的数组的首地址，该值是一个属性值，表示我们希望查询哪个属性的情况，第二个参数表示我们总共查询多少个属性的值，第3个参数表示这些属性都属于哪个属性集。接口方法的第三个参数返回当前我们总共查询到几个属性集的内容。第四个参数返回具体查到的属性值。多结果集接口的使用多结果集对象的定义如下：CoType TMultipleResults { [mandatory] interface IMultipleResults; [optional] interface ISupportErrorInfo; }一般在程序中，使用多结果集有如下步骤查询数据源是否支持多结果集，如果不支持则要考虑其他的实现方案如果它支持多结果集，在调用ICommandText接口的Execute方法执行SQL语句时，让其返回一个IMultipleRowset接口。循环调用接口的GetResult方法获取结果集对象。使用结果集对象最后是一个完整的例子://判断是否支持多结果集 BOOL SupportMultipleRowsets(IOpenRowset *pIOpenRowset) { COM_DECLARE_BUFFER(); COM_DECLARE_INTERFACE(IDBInitialize); COM_DECLARE_INTERFACE(IDBProperties); COM_DECLARE_INTERFACE(IGetDataSource); BOOL bRet = FALSE; //定义相关结构用于获取关于多结果集的信息 DBPROPID dbPropId[2] = {0}; DBPROPIDSET dbPropIDSet[1] = {0}; dbPropId[0] = DBPROP_MULTIPLERESULTS; dbPropIDSet[0].cPropertyIDs = 1; dbPropIDSet[0].guidPropertySet = DBPROPSET_DATASOURCEINFO; dbPropIDSet[0].rgPropertyIDs = dbPropId; //定义相关结构用于接受返回的属性信息 ULONG uPropsets = 0; DBPROPSET *pDBPropset = NULL; //获取数据源对象 HRESULT hRes = pIOpenRowset->QueryInterface(IID_IGetDataSource, (void**)&pIGetDataSource); COM_SUCCESS(hRes, _T("查询接口IGetDataSource失败，错误码为:%08x\n"), hRes); hRes = pIGetDataSource->GetDataSource(IID_IDBInitialize, (IUnknown **)&pIDBInitialize); COM_SUCCESS(hRes, _T("获取数据源对象失败，错误码为:%08x\n"), hRes); //获取对应属性 hRes = pIDBInitialize->QueryInterface(IID_IDBProperties, (void**)&pIDBProperties); COM_SUCCESS(hRes, _T("查询IDBProperties接口失败，错误码为:%08x\n"), hRes); hRes = pIDBProperties->GetProperties(1, dbPropIDSet, &uPropsets, &pDBPropset); COM_SUCCESS(hRes, _T("获取属性信息失败,错误码：%08x\n"), hRes); //判断是否支持多结果集 if (pDBPropset[0].rgProperties[0].vValue.vt == VT_I4) { if (pDBPropset[0].rgProperties[0].vValue.intVal & DBPROPVAL_MR_CONCURRENT) { COM_PRINT(_T("支持多结果集\n")); }else if (pDBPropset[0].rgProperties[0].vValue.intVal & DBPROPVAL_MR_SUPPORTED) { COM_PRINT(_T("支持多结果集\n")); }else if (pDBPropset[0].rgProperties[0].vValue.intVal & DBPROPVAL_MR_NOTSUPPORTED) { COM_PRINT(_T("不支持多结果集\n")); goto __CLEAR_UP; } else { COM_PRINT(_T("未知\n")); goto __CLEAR_UP; } } bRet = TRUE; __CLEAR_UP: SAFE_RELSEASE(pIDBInitialize); SAFE_RELSEASE(pIDBProperties); SAFE_RELSEASE(pIGetDataSource); CoTaskMemFree(pDBPropset[0].rgProperties); CoTaskMemFree(pDBPropset); return bRet; } // 执行sql语句并返回多结果集对象 BOOL ExecSQL(LPOLESTR lpSQL, IOpenRowset *pIOpenRowset, IMultipleResults* &pIMultipleResults) { COM_DECLARE_BUFFER(); COM_DECLARE_INTERFACE(IDBCreateCommand); COM_DECLARE_INTERFACE(ICommandText); COM_DECLARE_INTERFACE(ICommandProperties); BOOL bRet = FALSE; DBPROP dbProp[2] = {0}; DBPROPSET dbPropset[1] = {0}; dbProp[0].colid = DB_NULLID; dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; dbProp[0].dwPropertyID = DBPROP_UPDATABILITY; dbProp[0].vValue.vt = VT_I4; dbProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT; //设置SQL语句 HRESULT hRes = pIOpenRowset->QueryInterface(IID_IDBCreateCommand, (void**)&pIDBCreateCommand); COM_SUCCESS(hRes, _T("查询接口IDBCreateCommand失败，错误码:%08x\n"), hRes); hRes = pIDBCreateCommand->CreateCommand(NULL , IID_ICommandText, (IUnknown**)&pICommandText); COM_SUCCESS(hRes, _T("创建接口ICommandText失败,错误码:%08x"), hRes); hRes = pICommandText->SetCommandText(DBGUID_DEFAULT, lpSQL); COM_SUCCESS(hRes, _T("设置SQL语句失败,错误码为:%08x\n"), hRes); //设置属性 hRes = pICommandText->QueryInterface(IID_ICommandProperties, (void**)&pICommandProperties); COM_SUCCESS(hRes, _T("查询接口ICommandProperties接口失败,错误码:%08x\n"), hRes); //执行SQL语句 hRes = pICommandText->Execute(NULL, IID_IMultipleResults, NULL, NULL, (IUnknown**)&pIMultipleResults); COM_SUCCESS(hRes, _T("执行SQL语句失败,错误码:%08x\n"), hRes); bRet = TRUE; __CLEAR_UP: SAFE_RELSEASE(pIDBCreateCommand); SAFE_RELSEASE(pICommandText); SAFE_RELSEASE(pICommandProperties); return bRet; } int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); COM_DECLARE_BUFFER(); COM_DECLARE_INTERFACE(IOpenRowset); COM_DECLARE_INTERFACE(IMultipleResults); COM_DECLARE_INTERFACE(IRowset); LPOLESTR pSQL = OLESTR("Select * From aa26 Where Left(aac031,2) = '11';\ Select * From aa26 Where Left(aac031,2) = '32';\ Select * From aa26 Where Left(aac031,2) = '21';"); if (!CreateSession(pIOpenRowset)) { goto __EXIT; } if (!SupportMultipleRowsets(pIOpenRowset)) { goto __EXIT; } if (!ExecSQL(pSQL, pIOpenRowset, pIMultipleResults)) { goto __EXIT; } //循环取结果集 while (TRUE) { HRESULT hr = pIMultipleResults->GetResult(NULL, DBRESULTFLAG_DEFAULT, IID_IRowset, NULL, (IUnknown**)&pIRowset); if (hr != S_OK) { break; } //后面的代码就是针对每个结果集来进行列的绑定与数据输出，在这就不列举出来了 __CLEAR_UP: CoTaskMemFree(pdbColumn); CoTaskMemFree(pColumsName); FREE(pDBBinding); FREE(pData); pIAccessor->ReleaseAccessor(hAccessor, NULL); SAFE_RELSEASE(pIColumnsInfo); SAFE_RELSEASE(pIAccessor); SAFE_RELSEASE(pIRowset); _tsystem(_T("PAUSE")); } __EXIT: SAFE_RELSEASE(pIOpenRowset); SAFE_RELSEASE(pIMultipleResults); CoUninitialize(); return 0; }最后贴出例子完整代码的链接:源代码查看
最近根据项目需要，要在产品中添加对IpV6的支持，因此研究了一下IPV6的相关内容，Ipv6 与原来最直观的改变就是地址结构的改变，IP地址由原来的32位扩展为128，这样原来的地址结构肯定就不够用了，根据微软的官方文档，只需要对原来的代码做稍许改变就可以适应ipv6。修改地址结构Windows Socket2 针对Ipv6的官方描述根据微软官方的说法，要做到支持Ipv6首先要做的就是将原来的SOCKADDR_IN等地址结构替换为SOCKADDR_STORAGE 该结构的定义如下:typedef struct sockaddr_storage { short ss_family; char __ss_pad1[_SS_PAD1SIZE]; __int64 __ss_align; char __ss_pad2[_SS_PAD2SIZE]; } SOCKADDR_STORAGE, *PSOCKADDR_STORAGE;ss_family：代表的是地址家族，IP协议一般是AF_INET, 但是如果是IPV6的地址这个参数需要设置为 AF_INET6。后面的成员都是作为保留字段，或者说作为填充结构大小的字段，这个结构兼容了IPV6与IPV4的地址结构，跟以前的SOCKADDR_IN结构不同，我们现在不能直接从SOCKADDR_STORAGE结构中获取IP地址了。也没有办法直接往结构中填写IP地址。使用兼容函数除了地址结构的改变，还需要改变某些函数，有的函数是只支持Ipv4的，我们需要将这些函数改为即兼容的函数，根据官方的介绍，这些兼容函数主要是下面几个:WSAConnectByName : 可以直接通过主机名建立一个连接WSAConnectByList: 从一组主机名中建立一个连接getaddrinfo: 类似于gethostbyname, 但是gethostbyname只支持IPV4所以一般用这个函数来代替GetAdaptersAddresses: 这个函数用来代替原来的GetAdaptersInfoWSAConnectByName函数:函数原型如下:BOOL PASCAL WSAConnectByName( __in SOCKET s, __in LPSTR nodename, __in LPSTR servicename, __inout LPDWORD LocalAddressLength, __out LPSOCKADDR LocalAddress, __inout LPDWORD RemoteAddressLength, __out LPSOCKADDR RemoteAddress, __in const struct timeval* timeout, LPWSAOVERLAPPED Reserved ); s: 该参数为一个新创建的未绑定，未与其他主机建立连接的SOCKET，后续会采用这个socket来进行收发包的操作nodename: 主机名，或者主机的IP地址的字符串servicename: 服务名称，也可以是对应的端口号的字符串，传入服务名时需要传入那些知名的服务，比如HTTP、FTP等等， 其实这个字段本身就是需要传入端口的，传入服务名，最后函数会根据服务名称转化为这些服务的默认端口LocalAddressLength, LocalAddress， 返回当前地址结构，与长度RemoteAddressLength, RemoteAddress，返回远程主机的地址结构，与长度timeout: 超时值Reserved: 重叠IO结构为了使函数能够支持Ipv6，需要在调用前使用setsockopt函数对socket做相关设置，设置的代码如下:iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );调用函数的例子如下(该实例为微软官方的例子):SOCKET OpenAndConnect(LPWSTR NodeName, LPWSTR PortName) { SOCKET ConnSocket; DWORD ipv6only = 0; int iResult; BOOL bSuccess; SOCKADDR_STORAGE LocalAddr = {0}; SOCKADDR_STORAGE RemoteAddr = {0}; DWORD dwLocalAddr = sizeof(LocalAddr); DWORD dwRemoteAddr = sizeof(RemoteAddr); ConnSocket = socket(AF_INET6, SOCK_STREAM, 0); if (ConnSocket == INVALID_SOCKET){ return INVALID_SOCKET; } iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) ); if (iResult == SOCKET_ERROR){ closesocket(ConnSocket); return INVALID_SOCKET; } bSuccess = WSAConnectByName(ConnSocket, NodeName, PortName, &dwLocalAddr, (SOCKADDR*)&LocalAddr, &dwRemoteAddr, (SOCKADDR*)&RemoteAddr, NULL, NULL); if (bSuccess){ return ConnSocket; } else { return INVALID_SOCKET; } }WSAConnectByList该函数从传入的一组hostname中选取一个建立连接，函数内部会调用WSAConnectByName，它的原型，使用方式与WSAConnectByName类似，这里就不再给出具体的原型以及调用方法了。getaddrinfo该函数的作用与gethostbyname类似，但是它可以同时支持获取V4、V6的地址结构，函数原型如下:int getaddrinfo( const char FAR* nodename, const char FAR* servname, const struct addrinfo FAR* hints, struct addrinfo FAR* FAR* res );nodename: 主机名或者IP地址的字符串servname: 知名服务的名称或者端口的字符串hints：一个地址结构，该结构规定了应该如何进行地址转化。res：与gethostbyname类似，它也是返回一个地址结构的链表。后续只需要遍历这个链表即可。使用的实例如下:char szServer[] = "www.baidu.com"; char szPort[] = "80"; addrinfo hints = {0}; struct addrinfo* ai = NULL; getaddrinfo(szServer, szPort, NULL, &ai); while (NULL != ai) { SOCKET sConnect = socket(ai->ai_family, SOCK_STREAM, ai->ai_protocol); connect(sConnect, ai->ai_addr, ai->ai_addrlen); shutdown(sConnect, SD_BOTH); closesocket(sConnect); ai = ai->ai_next; } freeaddrinfo(ai); //最后别忘了释放链表针对硬编码的情况针对这种情况一般是修改硬编码，如果希望你的应用程序即支持IPV6也支持IPV4，那么就需要去掉这些硬编码的部分。微软提供了一个工具叫"Checkv4.exe" 这个工具一般是放到VS的安装目录中，作为工具一起安装到本机了，如果没有可以去官网下载。工具的使用也非常简单checkv4.exe 对应的.h或者.cpp 文件这样它会给出哪些代码需要进行修改，甚至会给出修改意见，我们只要根据它的提示修改代码即可。几个例子因为IPV6 不能再像V4那样直接往地址结构中填写IP了，因此在IPV6的场合需要大量使用getaddrinfo函数，来根据具体的IP字符串或者根据主机名来自动获取地址信息，然后根据地址信息直接调用connect即可，下面是微软的例子int ResolveName(char *Server, char *PortName, int Family, int SocketType) { int iResult = 0; ADDRINFO *AddrInfo = NULL; ADDRINFO *AI = NULL; ADDRINFO Hints; memset(&Hints, 0, sizeof(Hints)); Hints.ai_family = Family; Hints.ai_socktype = SocketType; iResult = getaddrinfo(Server, PortName, &Hints, &AddrInfo); if (iResult != 0) { printf("Cannot resolve address [%s] and port [%s], error %d: %s\n", Server, PortName, WSAGetLastError(), gai_strerror(iResult)); return SOCKET_ERROR; } if(NULL != AddrInfo) { SOCKET sConnect = socket(AddrInfo->ai_family, SOCK_STREAM, AddrInfo->ai_protocol); connect(sConnect, AddrInfo->ai_addr, AddrInfo->ai_addrlen); shutdown(sConnect, SD_BOTH); closesocket(sConnect); } freeaddrinfo(AddrInfo); return 0; }这个例子需要传入额外的family参数来规定它使用何种地址结构，但是如果我只有一个主机名，而且事先并不知道需要使用何种IP协议来进行通信，这种情况下又该如何呢？针对服务端，不存在这个问题，服务端是我们自己的代码，具体使用IPV6还是IPV4这个实现是可以确定的，因此可以采用跟上面类似的写法:BOOL Create(int af_family) { //这里不建议使用IPPROTO_IP 或者IPPROTO_IPV6，使用TCP或者UDP可能会好点，因为它们是建立在IP协议之上的 //当然,具体情况具体分析 s = socket(af_family, SOCK_STREAM, IPPROTO_TCP); } BOOL Bind(int family, UINT nPort) { addrinfo hins = {0}; hins.ai_family = family; hins.ai_flags = AI_PASSIVE; /* For wildcard IP address */ hins.ai_protocol = IPPROTO_TCP; hins.ai_socktype = SOCK_STREAM; addrinfo *lpAddr = NULL; CString csPort = ""; csPort.Format("%u", nPort); if (0 != getaddrinfo(NULL, csPort, &hins, &lpAddr)) { closesocket(s); return FALSE; } int nRes = bind(s, lpAddr->ai_addr, lpAddr->ai_addrlen); freeaddrinfo(lpAddr); if(nRes == 0) return TRUE; return FALSE; } //监听，以及后面的收发包并没有区分V4和V6，因此这里不再给出跟他们相关的代码针对服务端，我们自然没办法事先知道它使用的IP协议的版本，因此传入af_family参数在这里不再适用，我们可以利用getaddrinfo函数根据服务端的主机名或者端口号来提前获取它的地址信息，这里我们可以封装一个函数int GetAF_FamilyByHost(LPCTSTR lpHost, int nPort, int SocketType) { addrinfo hins = {0}; addrinfo *lpAddr = NULL; hins.ai_family = AF_UNSPEC; hins.ai_socktype = SOCK_STREAM; hins.ai_protocol = IPPROTO_TCP; CString csPort = ""; csPort.Format("%u", nPort); int af = AF_UNSPEC; char host[MAX_HOSTNAME_LEN] = ""; if (lpHost == NULL) { gethostname(host, MAX_HOSTNAME_LEN);// 如果为NULL 则获取本机的IP地址信息 }else { strcpy_s(host, MAX_HOSTNAME_LEN, lpHost); } if(0 != getaddrinfo(host, csPort, &hins, &lpAddr)) { return af; } af = lpAddr->ai_family; freeaddrinfo(lpAddr); return af; }有了地址家族信息，后面的代码即可以根据地址家族信息来分别处理IP协议的不同版本，也可以使用上述服务端的思路，直接使用getaddrinfo函数得到的addrinfo结构中地址信息，下面给出第二种思路的部分代码:if(0 != getaddrinfo(host, csPort, &hins, &lpAddr)) { connect(s, lpAddr->ai_addr, lpAddr->ai_addrlen); }当然，也可以使用前面提到的 WSAConnectByName 函数，不过它需要针对IPV6来进行特殊的处理，需要事先知道服务端的IP协议的版本。VC中各种地址结构在学习网络编程中，一个重要的概念就是IP地址，而巴克利套接字中提供了好几种结构体来表示地址结构，微软针对WinSock2 又提供了一些新的结构体，有的时候众多的结构体让人眼花缭乱，在这我根据自己的理解简单的回顾一下这些常见的结构SOCKADD_IN 与sockaddr_in结构在Winsock2 中这二者是等价的， 它们的定义如下:struct sockaddr_in{ short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };sin_family: 地址协议家族sin_port：端口号sin_addr: 表示ip地址的结构sin_zero: 用于与sockaddr 结构体的大小对齐，这个数组里面为全0in_addr 结构如下:struct in_addr { union { struct{ unsigned char s_b1, s_b2, s_b3, s_b4; } S_un_b; struct { unsigned short s_w1, s_w2; } S_un_w; unsigned long S_addr; } S_un; }; 这个结构是一个公用体，占4个字节，从本质上将IP地址仅仅是一个占4个字节的无符号整型数据，为了方便读写才会采用点分十进制的方式。仔细观察这个结构会发现，它其实定义了IP地址的几种表现形式，我们可以将IP地址以一个字节一个字节的方式拆开来看，也可以以两个字型数据的形式拆开，也可以简单的看做一个无符号长整型。当然在写入的时候按照这几种方式写入，为了方便写入IP地址，微软定义了一个宏:#define s_addr S_un.S_addr因此在填入IP地址的时候可以简单的使用这个宏来给S_addr这个共用体成员赋值一般像bind、connect等函数需要传入地址结构，它们需要的结构为sockaddr，但是为了方便都会传入SOCKADDR_IN结构sockaddr SOCKADDR结构这两个结构也是等价的，它们的定义如下struct sockaddr { unsigned short sa_family; char sa_data[14];};从结构上看它占16个字节与 SOCKADDR_IN大小相同，而且第一个成员都是地址家族的相关信息，后面都是存储的具体的IPV4的地址，因此它们是可以转化的，为了方便一般是使用SOCKADDR_IN来保存IP地址，然后在需要填入SOCKADDR的时候强制转化即可。sockaddr_in6该结构类似于sockaddr_in，只不过它表示的是IPV6的地址信息，在使用上，由于IPV6是128的地址占16个字节，而sockaddr_in 中表示地址的部分只有4个字节，所以它与之前的两个是不能转化的，在使用IPV6的时候需要特殊的处理，一般不直接填写IP而是直接根据IP的字符串或者主机名来连接。sockaddr_storage这是一个通用的地址结构，既可以用来存储IPV4地址也可以存储IPV6的地址，这个地址结构在前面已经说过了，这里就不再详细解释了。各种地址之间的转化一般我们只使用从SOCKADDR_IN到sockaddr结构的转化，而且仔细观察socket函数族发现只需要从其他结构中得到sockaddr结构，而并不需要从sockaddr转化为其他结构，因此这里重点放在如何转化为sockaddr结构从SOCKADDR_IN到sockaddr只需要强制类型转化即可从addrinfo结构中只需要调用其成员即可从SOCKADDR_STORAGE结构到sockaddr只需要强制转化即可。其实在使用上更常用的是将字符串的IP转化为对应的数值，针对IPV4有我们常见的inet_addr、inet_ntoa 函数,它们都是在ipv4中使用的，针对v6一般使用inet_pton,inet_ntop来转化，它们两个分别对应于inet_addr、inet_ntoa。但是在WinSock中更常用的是WSAAddressToString 与 WSAStringToAddressINT WSAAddressToString( LPSOCKADDR lpsaAddress, DWORD dwAddressLength, LPWSAPROTOCOL_INFO lpProtocolInfo, OUT LPTSTR lpszAddressString, IN OUT LPDWORD lpdwAddressStringLength );lpsaAddress: ip地址dwAddressLength: 地址结构的长度lpProtocolInfo: 协议信息的结构体，这个结构一般给NULLlpszAddressString：目标字符串的缓冲lpdwAddressStringLength:字符串的长度而WSAStringToAddress定义与使用与它类似，这里就不再说明了。
木马和病毒的好坏很大程度上取决于它的隐蔽性，木马和病毒本质上也是在执行程序代码，如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现，在编写这类程序的时候可以考虑将代码注入到其他进程中，借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用，防病毒软件和软件调试中也有很大的用途，最近也简单的研究过这些东西，在这里将它发布出来。想要将代码注入到其他进程并能成功执行需要解决两个问题：第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑，想要破坏原来的执行流程，使EIP寄存器跳转到注入的代码位置基本是不可能的第二个问题是每个进程中地址空间是独立的，比如在调用某个句柄时，即使是同一个内核对象，在不同进程中对应的句柄也是不同的，这就需要进行地址转化。要进行远程代码注入的要点和难点主要就是这两个问题，下面给出两种不同的注入方式来说明如何解决这两个问题DLL注入DLL注入很好的解决了第二个问题，DLL被加载到目标进程之后，它里面的代码中的地址就会自动被转化为对应进程中的地址，这个特性是由于DLL加载的过程决定的，它会自己使用它所在进程中的资源和地址空间，所以只要DLL中不存在硬编码的地址，基本不用担心里面会出现函数或者句柄需要进行地址转化的问题。那么第一个问题改怎么解决呢？要执行用户代码，在Windows中最常见的就是使用回调的方式，Windows采用的是事件驱动的方式，只要发生了某些事件就会调用回调，在众多使用回调的场景中，线程的回调是最简单的，它不会干扰到目标进程的正常执行，也就不用考虑最后还原EIP的问题，因此DLL注入采用的最常见的就是创建一个远程线程，让线程加载DLL代码。DLL注入中一般的思路是：使用CreateRemoteThread来在目标进程中创建一个远程的线程，这个线程主要是加载DLL到目标进程中，由于DLL在入口函数(DLLMain)中会处理进程加载Dll的事件，所以将注入代码写到这个事件中，这样就能执行注入的代码了。那么如何在远程进程中执行DLL的加载操作呢？我们知道加载DLL主要使用的是函数LoadLibrary,仔细分析线程的回调函数和LoadLibrary函数的原型，会发现，它们同样都是传入一个参数，而CreateRemoteThread函数正好需要一个函数的地址作为回调，并且传入一个参数作为回调函数的参数。这样就有思路了，我们让LoadLibrary作为线程的回调函数，将对应dll的文件名和路径作为参数传入，这样就可以在对应进程中加载dll了，进一步也就可以执行dllmain中的对应代码了。还有一个很重要的问题，我们知道不同进程中，地址空间是隔离的，那么我在注入的进程中传入LoadLibrary函数的地址，这算是一个硬编码的地址，它在目标进程中是否是一样的呢？答案是，二者的地址是一样的，这是由于kernel32.dll在32位程序中加载的基地址是一样的，而LoadLibrary在kernel32.dll中的偏移是一定的（只要不同的进程加载的是同一份kernel32.dll）那么不同进程中的LoadLibrary函数的地址是一样的。其实不光是LoadLibrary函数，只要是kernel32.dll中导出的函数，在不同进程中的地址都是一样的。注意这里只是32位，如果想要使用32位程序往64位目标程序中注入，可能需要考虑地址转换的问题，只要知道kernel32.dll在64位中的偏移，就可以计算出对应函数的地址了。LoabLibrary函数传入的代表路径的字符串的首地址在不同进程中同样是不同的，而且也没办法利用偏移来计算，这个时候解决的办法就是在远程进程中申请一块虚拟地址空间，并将目标字符串写入对应的地址中，然后将对应的首地址作为参数传入即可。最后总结一下DLL注入的步骤：获取LoadLibrary函数的地址调用VirtualAllocEx 函数在远程进程中申请一段虚拟内存调用WriteProcessMemory 函数将参数写入对应的虚拟内存调用CreateRemoteThread 函数创建远程线程，线程的回调函数为LoadLibrary，参数为对应的字符串的地址按照这个思路可以编写如下的代码:typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR); if (!DebugPrivilege()) //提权代码，在Windows Vista 及以上的版本需要将进程的权限提升，否则打开进程会失败 { return FALSE; } //打开目标进程 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //dwPid是对应的进程ID if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); } //查找LoadLibrary函数地址 pfnLoadLibrary lpLoadLibrary = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW"); //在远程进程中申请一块内存用于保存对应线程的参数 PVOID pBuffer = VirtualAllocEx(hRemoteProcess, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE); //在对应内存位置处写入参数值 DWORD dwWritten = 0; WriteProcessMemory(hRemoteProcess, pBuffer, m_csDLLName.GetString(), (m_csDLLName.GetLength() + 1) * sizeof(TCHAR), &dwWritten); //创建远程线程并传入对应参数 HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpLoadLibrary, pBuffer, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, pBuffer, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);卸载远程DLL上面进行了代码的注入，作为一个文明的程序，自然得考虑卸载dll，毕竟现在提倡环保，谁使用，谁治理。这里既然注入了，自然得考虑卸载。卸载的思路与注入的类似，只是函数变为了FreeLibrary，传入的参数变成了对应的dll的句柄了。如何获取这个模块的句柄呢？我们可以枚举进程中的模块，根据模块的名称来找到对应的模块并获取它的句柄。枚举的方式一般是使用toolhelp32中对应的函数，下面是卸载的例子代码HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, m_dwPid); if (INVALID_HANDLE_VALUE == hSnapshot) { AfxMessageBox(_T("CreateToolhelp32Snapshot Error")); return; } MODULEENTRY32 me = {0}; me.dwSize = sizeof(MODULEENTRY32); BOOL bRet = Module32First(hSnapshot, &me); while (bRet) { CString csModuleFile = _tcsupr(me.szExePath); if (csModuleFile == _tcsupr((LPTSTR)m_csDLLName.GetString()) != -1) { break; } ZeroMemory(&me, sizeof(me)); me.dwSize = sizeof(PROCESSENTRY32); bRet = Module32Next(hSnapshot, &me); } CloseHandle(hSnapshot); typedef BOOL (*pfnFreeLibrary)(HMODULE); pfnFreeLibrary FreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "FreeLibrary"); HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwPid); if (hRemoteProcess == NULL) { AfxMessageBox(_T("OpenProcess Error")); return; } HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, me.modBaseAddr, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);无DLL的注入注入不一定需要使用DLL，虽然使用DLL比较简单一点，无DLL注入在解决上述两个问题的第一个思路是一样的，也是使用CreateRemoteThread来创建一个远程线程来执行目标代码。无dll的注入主要麻烦是在进行地址转化上，在调用API的时候，如果无法保证对应的dll的基地址不变的话，就得在目标进程中自行调用LoadLibrary来动态获取函数地址，并调用。在动态获取API函数的地址的时候，主要使用的函数是LoadLibrary、GetModuleHandle、GetProcAddress这三个函数，而线程的回调函数只能传入一个参数，所以我们需要将对应的需要传入的参数组成一个结构体，并将结构体对应的数据写入到目标进程的内存中，特别要注意的是，里面不要使用指针或者句柄这种与地址有关的东西。例如我们想在目标进程中注入一段代码，让它弹出一个对话框，以便测试是否注入成功。这种情况除了要传入上述三个函数的地址外，还需要MesageBox，而MessageBox是在user32.dll中，user32.dll在每个进程中的基地址并不相同，因此在注入的代码中需要动态加载，因此可以定义下面一个结构typedef struct REMOTE_DATA { DWORD dwLoadLibrary; DWORD dwGetProcAddress; DWORD dwGetModuleHandle; DWORD dwGetModuelFileName; //辅助函数 char szUser32dll[MAX_PATH]; //存储user32dll的路径，以便调用LoadLibrary加载 char szMessageBox[128]; //存储字符串MessageBoxA 这个字符串，以便使用GetProcAddress加载MesageBox函数 char szMessage[512]; //弹出对话框上显示的字符 }不使用DLL注入与使用DLL注入的另一个区别是，不使用DLL注入的时候需要自己加载目标代码到对应的进程中，这个操作可以借由WriteProcessMemory 将函数代码写到对应的虚拟内存中。最后注入的代码主要如下:DWORD WINAPI RemoteThreadProc(LPVOID lpParam) { LPREMOTE_DATA lpData = (LPREMOTE_DATA)lpParam; typedef HMODULE (WINAPI *pfnLoadLibrary)(LPCSTR); typedef FARPROC (WINAPI *pfnGetProcAddress)(HMODULE, LPCSTR); typedef HMODULE (*pfnGetModuleHandle)(LPCSTR); typedef DWORD (WINAPI *pfnGetModuleFileName)( HMODULE,LPSTR, DWORD); pfnGetModuleHandle MyGetModuleHandle = (pfnGetModuleHandle)lpData->dwGetModuleHandle; pfnGetModuleFileName MyGetModuleFileName = (pfnGetModuleFileName)lpData->dwGetModuleFileName; pfnGetProcAddress MyGetProcAddress = (pfnGetProcAddress)lpData->dwGetProcAddress; pfnLoadLibrary MyLoadLibrary = (pfnLoadLibrary)lpData->dwGetProcAddress; typedef int (WINAPI *pfnMessageBox)(HWND, LPCSTR, LPCSTR, UINT); //加载User32.dll HMODULE hUser32Dll = MyLoadLibrary(lpData->szUerDll); //加载MessageBox函数 pfnMessageBox MyMessageBox = (pfnMessageBox)MyGetProcAddress(hUser32Dll, lpData->szMessageBox); char szTitlte[MAX_PATH] = ""; MyGetModuleFileName(NULL, szTitlte, MAX_PATH); MyMessageBox(NULL, lpData->szMessage, szTitlte, MB_OK); return 0; } m_dwPid = GetPid(); //获取目标进程ID DebugPrivilege(); //进程提权 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwPid); if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); return; } LPREMOTE_DATA lpData = new REMOTE_DATA; ZeroMemory(lpData, sizeof(REMOTE_DATA)); //获取对应函数的地址 lpData->dwGetModuleFileName = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleFileNameA"); lpData->dwGetModuleHandle = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleHandleA"); lpData->dwGetProcAddress = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetProcAddress"); lpData->dwLoadLibrary = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryA"); // 拷贝对应的字符串 StringCchCopyA(lpData->szMessage, MAX_STRING_LENGTH, "Inject Success!!!"); StringCchCopyA(lpData->szUerDll, MAX_PATH, "user32.dll"); StringCchCopyA(lpData->szMessageBox, MAX_PROC_NAME_LENGTH, "MessageBoxA"); //在远程空间中申请对应的内存，写入参数和函数的代码 LPVOID lpRemoteBuf = VirtualAllocEx(hRemoteProcess, NULL, sizeof(REMOTE_DATA), MEM_COMMIT, PAGE_READWRITE); // 存储data结构的数据 LPVOID lpRemoteProc = VirtualAllocEx(hRemoteProcess, NULL, 0x4000, MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 存储函数的代码 DWORD dwWrittenSize = 0; WriteProcessMemory(hRemoteProcess, lpRemoteProc, &RemoteThreadProc, 0x4000, &dwWrittenSize); WriteProcessMemory(hRemoteProcess, lpRemoteBuf, lpData, sizeof(REMOTE_DATA), &dwWrittenSize); HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpRemoteProc, lpRemoteBuf, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, lpRemoteBuf, 0, MEM_RELEASE); VirtualFreeEx(hRemoteProcess, lpRemoteProc, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess); delete[] lpData;
我们在程序发布后总会面临崩溃的情况，这个时候一般很难重现或者很难定位到程序崩溃的位置，之前有方法在程序崩溃的时候记录dump文件然后通过windbg来分析。那种方法对开发人员的要求较高，它需要程序员理解内存、寄存器等等一系列概念还需要手动加载对应的符号表。Java、Python等等语言在崩溃的时候都会打印一条异常的堆栈信息并告诉用户那块出错了，根据这个信息程序员可以很容易找到对应的代码位置并进行处理，而C/C++则会弹出一个框告诉用户程序崩溃了，二者对比来看，C++似乎对用户太不友好了，而且根据它的弹框很难找到对应的问题，那么有没有可能使c++像Java那样打印异常的堆栈呢？这个自然是可能的，本文就是要讨论如何在Windows上实现类似的功能异常处理一般当程序发生异常时，用户代码停止执行，并将CPU的控制权转交给操作系统，操作系统接到控制权后，将当前线程的环境保存到结构体CONTEXT中，然后查找针对此异常的处理函数。系统利用结构EXCEPTION_RECORD保存了异常描述信息，它与CONTEXT一同构成了结构体EXCEPTION_POINTERS，一般在异常处理中经常使用这个结构体。异常信息EXCEPTION_RECORD的定义如下：typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常码 DWORD ExceptionFlags; //标志异常是否继续，标志异常处理完成后是否接着之前有问题的代码 struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一个异常节点的指针，这是一个链表结构 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //异常附加信息 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的字符串 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;Windows平台提供的这一套异常处理的机制，我们叫它结构化异常处理(SEH)，它的处理过程一般如下:如果程序是被调试运行的（比如我们在VS编译器中调试运行程序），当异常发生时，系统首先将异常信息交给调试程序，如果调试程序处理了那么程序继续运行，否则系统便在发生异常的线程栈中查找可能的处理代码。若找到则处理异常，并继续运行程序如果在线程栈中没有找到，则再次通知调试程序，如果这个时候仍然不能处理这个异常，那么操作系统会对异常进程默认处理，这个时候一般都是直接弹出一个错误的对话框然后终止程序。系统在每个线程的堆栈环境中都维护了一个SEH表，表中是用户注册的异常类型以及它对应的处理函数，每当用户在函数中注册新的异常处理函数，那么这个信息会被保存在链表的头部，也就是说它是采用头插法来插入新的处理函数，从这个角度上来说，我们可以很容易理解为什么在一般的高级语言中一般会先找与try块最近的catch块，然后在找它的上层catch，由里到外依次查找。与try块最近的catch是最后注册的，由于采用的是头插法，自然它会被首先处理。在Windows中针对异常处理，扩展了__try 和 __except 两个操作符，这两个操作符与c++中的try和catch非常相似,作用也基本类似，它的一般的语法结构如下:__try { //do something } __except(filter) { //handle }使用 __try 和 __except 的时候它主要分为3个部分，分别为:保护代码体、过滤表达式、异常处理块保护代码体一般是try中的语句，它值被保护的代码，也就是说我们希望处理那个代码块产生的异常过滤表达式是 except后面扩号中的值，它只能是3个值中的一个，EXCEPTION_CONTINUE_SEARCH继续向下查找异常处理，也就是说这里的异常处理块不处理这种异常，EXCEPTION_CONTINUE_EXECUTION表示异常已被处理，这个时候可以继续执行直线产生异常的代码，EXCEPTION_EXECUTE_HANDLER表示异常已被处理，此时直接跳转到except里面的代码块中，这种方式下它的执行流程与一般的异常处理的流程类似.异常处理块，指的是except下面的扩号中的代码块.注意：我们说过滤表达式只能是这三个值中的一个，但是没有说这里一定得填这三个值，它还支持函数或者其他的表达式类型，只要函数或者表达式的返回值是这三个值中的一个即可。上述的方式也有他的局限性，也就是说它只能保护我们指定的代码，如果是在 __try 块之外的代码发生了崩溃，可能还是会造成程序被kill掉，而且每个位置都需要写上这么些代码实在是太麻烦了。其实处理异常还有一种方式，那就是采用 SetUnhandledExceptionFilter来注册一个全局的异常处理函数来处理所有未被处理的异常，其实它的主要工作原理就是往异常处理的链表头上添加一个处理函数，函数的原型如下:LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);它需要传入一个函数，以便发生异常的时候调用这个函数，这个回调函数的原型如下:LONG WINAPI UnhandledExceptionFilter( __in struct _EXCEPTION_POINTERS* ExceptionInfo );回调函数会传入一个表示当前堆栈和异常信息的结构体的指针，结构的具体信息请参考MSDN, 函数会返回一个long型的数值，这个数值为上述3个值中的一个，表示当系统调用了这个异常处理函数处理异常之后该如何继续执行用户代码。SetUnhandledExceptionFilter 函数返回一个函数指针，这个指针指向链表的头部，如果插入处理函数失败那么它将指向原来的链表头，否则指向新的链表头(也就是注册的这个回调函数的地址)而这次要实现这么一个能打印异常信息和调用堆栈的功能就是要使用这个方法。打印函数调用堆栈关于打印堆栈的内容，这里不再多说了，请参考本人之前写的博客windows平台调用函数堆栈的追踪方法这里的主要思路是使用StackWalker来根据当前的堆栈环境来获取对应的函数信息，这个信息需要根据符号表来生成，因此我们需要首先加载符号表，而获取当前线程的环境，我们可以像我博客中写的那样使用GetThreadContext来获取，但是在异常中就简单的多了，还记得异常处理函数的原型吗？异常处理函数本身会带入一个EXCEPTION_POINTERS结构的指针，而这个结构中就包含了异常堆栈的信息。还有一些需要注意的问题，我把它放到实现那块了，请小心的往下看^_^实现实现部分的源码我放到了github上，地址这个项目中主要分为两个类CBaseException，主要是对异常的一个简单的封装，提供了我们需要的一些功能，比如获取加载的模块的信息，获取调用的堆栈，以及解析发生异常时的相关信息。而这些的基础都在CStackWalker中。使用上，我把CBaseException中的大部分函数都定义成了virtual 允许进行重写。因为具体我还没想好这块后续会需要进行哪些扩展。但是里面最主要的功能是OutputString函数，这个函数是用来进行信息输出的，默认CBaseException是将信息输出到控制台上，后续可以重载这个函数把数据输出到日志中。CBaseException 类CBaseException 主要是用来处理异常，在代码里面我提供了两种方式来进行异常处理，第一种是通过 SetUnhandledExceptionFilter 来注册一个全局的处理函数，这个函数是类中的静态函数UnhandledExceptionFilter，在这个函数中我主要根据异常的堆栈环境来初始化了一个CBaseException类，然后简单的调用类的方法显示异常与堆栈的相关信息。第二种是通过 _set_se_translator 来注册一个将SEH转化为C++异常的方法，在对应的回调中我简单的抛出了一个CBaseException的异常，在具体的代码中只要简单的用c++的异常处理捕获这么一个异常即可CBaseException 类中主要用来解析异常的信息，里面提供这样功能的函数主要有3个ShowExceptionResoult: 这个函数主要是根据异常码来获取到异常的具体字符串信息，比如非法内存访问、除0异常等等GetLogicalAddress：根据发生异常的代码的地址来获取对应的模块信息，比如它在PE文件中属于第几个节，节的地址范围等等，它在实现上首先使用 VirtualQuery来获取对应的虚拟内存信息，主要是这个模块的首地址信息，然后解析PE文件获取节表的信息，我们循环节表中的每一项，根据节表中的地址范围来判断它属于第几个节，注意这里我们根据它在内存中的偏移计算了它在PE文件中的偏移，具体的计算方式请参考PE文件的相关内容.3.ShowRegistorInformation：获取各个寄存器的值，这个值保存在CONTEXT结构中，我们只需要简单打印它就好CStackWalker类这个类主要实现一些基础的功能，它主要提供了初始化符号表环境、获取对应的调用堆栈信息、获取加载的模块信息在初始化符号表的时候尽可以多的遍历了常见的几种符号表的位置并将这些位置中的符号表加载进来，以便能更好的获取到堆栈调用的情况。在获取到对应的符号表位置后有这样的代码if (NULL != m_lpszSymbolPath) { m_bSymbolLoaded = SymInitialize(m_hProcess, T2A(m_lpszSymbolPath), TRUE); //这里设置为TRUE，让它在初始化符号表的同时加载符号表 } DWORD symOptions = SymGetOptions(); symOptions |= SYMOPT_LOAD_LINES; symOptions |= SYMOPT_FAIL_CRITICAL_ERRORS; symOptions |= SYMOPT_DEBUG; SymSetOptions(symOptions); return m_bSymbolLoaded;这里将 SymInitialize的最后一个函数置为TRUE，这个参数的意思是是否枚举加载的模块并加载对应的符号表，直接在开始的时候加载上可能会比较浪费内存，这个时候我们可以采用动态加载的方式，在初始化的时候先填入FALSE，然后在需要的时候自己枚举所有的模块，然后手动加载所有模块的符号表，手动加载需要调用SymLoadModuleEx。这里需要提醒各位的是，这里如果填的是FALSE的话，后续一定得自己加载模块的符号表，否则在后续调用SymGetSymFromAddr64的时候会得到一堆的487错误（也就是地址无效）我之前就是这个问题困扰了我很久的时间。在获取模块的信息时主要提供了两种方式，一种是使用CreateToolhelp32Snapshot 函数来获取进程中模块信息的快照然后调用Module32Next 和 Module32First来枚举模块信息，还有一种是使用EnumProcessModules来获取所有模块的句柄，然后根据句柄来获取模块的信息，当然还有另外的方式，其他的方式可以参考我的这篇博客 枚举进程中的模块在枚举加载的模块的同时还针对每个模块调用了 GetModuleInformation 函数，这个函数主要有两个功能，获取模块文件的版本号和获取加载的符号表信息。接下来就是重头戏了——获取调用堆栈。获取调用堆栈首先得获取当前的环境，在代码中进行了相应的判断，如果当前传入的CONTEXT为NULL，则函数自己获取当前的堆栈信息。在获取堆栈信息的时候首先判断是否为当前线程，如果不是那么为了结果准确，需要先停止目标线程，然后获取，否则直接使用宏来获取，对应的宏定义如下:#define GET_CURRENT_THREAD_CONTEXT(c, contextFlags) \ do\ {\ memset(&c, 0, sizeof(CONTEXT));\ c.ContextFlags = contextFlags;\ __asm call $+5\ __asm pop eax\ __asm mov c.Eip, eax\ __asm mov c.Ebp, ebp\ __asm mov c.Esp, esp\ } while (0)在调用StackWalker时只需要关注esp ebp eip的信息，所以这里我们也只简单的获取这些寄存器的环境，而其他的就不管了。这样有一个问题，就是我们是在CStackWalker类中的函数中获取的这个线程环境，那么这个环境里面会包含CStackWalker::StackWalker，结果自然与我们想要的不太一样（我们想要的是隐藏这个库中的相关信息，而只保留调用者的相关堆栈信息）。这个问题我还没有什么好的解决方案。在获取到线程环境后就是简单的调用StackWalker以及那堆Sym开头的函数来获取各种信息了，这里就不再详细说明了。至此这个功能已经实现的差不多了。库的具体使用请参考main.cpp这个文件，相信有这篇博文以及源码各位应该很容易就能够使用它。据说这些函数不是多线程安全的，我自己没有在多线程环境下进行测试，所以具体它在多线程环境下表现如何还是个未知数，如果后续我有兴趣继续完善它的话，可能会加入多线程的支持。
之前在WinSock2.0 API 中说到，像DisConnectEx 函数这样，它具有回收SOCKET的功能，而像AcceptEx这样的函数，它不会自己在内部创建新的SOCKET，需要外部传入SOCKET作为传输数据用的SOCEKT，使用这两个函数，我们可以做到，事先创建大量的SOCKET，然后使用AcceptEx函数从创建的SOCKET中选择一个作为连接用的SOCKET，在不用这个SOCKET的时候使用DisConnectEx回收。这样的功能就是一个SOCKET池的功能。SOCKET池WinSock 函数就是为了提升程序的性能而产生的，这些函数主要使用与TCP协议，我们可以在程序启动的时候创建大量的SOCKET句柄，在必要的时候直接使用AcceptEx这样的函数来使用已有的SOCKET作为与客户端的连接，在适当的时候使用WSARecv、WSASend等函数金星秀数据收发操作。而在不用SOCKET的时候使用DisConnectEx 回收，这样在响应用户请求的时候就省去了大量SOCKET创建和销毁的时间，从而提高的响应速度。IOCP本身也是一个线程池，如果用它结合WinSock 的线程池将会是Windows系统上最佳的性能组合，当然在此基础上可以考虑加入线程池、内存池的相关技术来进一步提高程序的性能。这里我想顺便扯点关于程序优化的理解。程序优化主要考虑对函数进行优化，毕竟在C/C++中函数是最常用，最基本的语法块，它的使用十分常见。函数的优化一般有下面几个需要考虑的部分是否需要大量调用这个函数。针对需要大量调用某个函数的情况，可以考虑对算法进行优化，减少函数的调用函数中是否有耗时的操作，如果有可以考虑使用异步的方式，或者将函数中的任务放到另外的线程（只有当我们确实不关心这个耗时操作的结果的时候，也就是说不涉及到同步的时候）函数中是否有大量的资源调用，如果有，可以考虑使用资源池的方式避免大量资源的申请与释放操作下面是一个使用SOCKET池的客户端的实例#include <stdio.h> #include <process.h> #include "MSScokFunc.h" #define SERVICE_IP "119.75.213.61" #define MAX_CONNECT_SOCKET 500 unsigned int WINAPI IOCPThreadProc(LPVOID lpParam); LONG g_nPorts = 0; CMSScokFunc g_MsSockFunc; typedef struct _tag_CLEINT_OVERLAPPED { OVERLAPPED overlapped; ULONG ulNetworkEvents; DWORD dwFlags; DWORD wConnectPort; DWORD dwTransBytes; char *pBuf; DWORD dwBufSize; SOCKET sConnect; }CLIENT_OVERLAPPED, *LPCLIENT_OVERLAPPED; SOCKADDR_IN g_LocalSockAddr = {AF_INET}; int main() { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SYSTEM_INFO si = {0}; const int on = 1; GetSystemInfo(&si); HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, si.dwNumberOfProcessors); HANDLE *pThreadArray = (HANDLE *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(HANDLE) * 2 * si.dwNumberOfProcessors); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, IOCPThreadProc, &hIOCP, 0, NULL); pThreadArray[i] = hThread; } g_MsSockFunc.LoadAllFunc(AF_INET, SOCK_STREAM, IPPROTO_IP); LPCLIENT_OVERLAPPED *pOverlappedArray = (LPCLIENT_OVERLAPPED *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(LPCLIENT_OVERLAPPED) * MAX_CONNECT_SOCKET); SOCKET *pSocketsArray = (SOCKET *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKET) * MAX_CONNECT_SOCKET); int nIndex = 0; g_LocalSockAddr.sin_addr.s_addr = INADDR_ANY; g_LocalSockAddr.sin_port = htons(0); //让系统自动分配 printf("开始端口扫描........\n"); for (int i = 0; i < MAX_CONNECT_SOCKET; i++) { g_nPorts++; SOCKET sConnectSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, NULL, WSA_FLAG_OVERLAPPED); //允许地址重用 setsockopt(sConnectSock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); bind(sConnectSock, (SOCKADDR*)&g_LocalSockAddr, sizeof(SOCKADDR_IN)); SOCKADDR_IN sockAddr = {0}; sockAddr.sin_addr.s_addr = inet_addr(SERVICE_IP); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(g_nPorts); LPCLIENT_OVERLAPPED lpoc = (LPCLIENT_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CLIENT_OVERLAPPED)); lpoc->ulNetworkEvents = FD_CONNECT; lpoc->wConnectPort = g_nPorts; lpoc->sConnect = sConnectSock; lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKADDR_IN)); lpoc->dwBufSize = sizeof(SOCKADDR_IN); CopyMemory(lpoc->pBuf, &sockAddr, sizeof(SOCKADDR_IN)); if(!g_MsSockFunc.ConnectEx(sConnectSock, (SOCKADDR*)lpoc->pBuf, sizeof(SOCKADDR_IN), NULL, 0, &lpoc->dwTransBytes, &lpoc->overlapped)) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf("第(%d)个socket调用ConnectEx失败, 错误码为:%08x\n", i, WSAGetLastError()); HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, lpoc); closesocket(sConnectSock); continue; } } CreateIoCompletionPort((HANDLE)sConnectSock, hIOCP, NULL, 0); pSocketsArray[nIndex] = sConnectSock; pOverlappedArray[nIndex] = lpoc; nIndex++; } g_nPorts = nIndex; WaitForMultipleObjects(2 * si.dwNumberOfProcessors, pThreadArray, TRUE, INFINITE); printf("端口扫描结束.......\n"); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { CloseHandle(pThreadArray[i]); } HeapFree(GetProcessHeap(), 0, pThreadArray); printf("清理对应线程完成........\n"); CloseHandle(hIOCP); printf("清理完成端口句柄完成........\n"); for (int i = 0; i < nIndex; i++) { closesocket(pSocketsArray[i]); HeapFree(GetProcessHeap(), 0, pOverlappedArray[i]); } HeapFree(GetProcessHeap(), 0, pSocketsArray); HeapFree(GetProcessHeap(), 0, pOverlappedArray); printf("清理sockets池成功...............\n"); WSACleanup(); return 0; } unsigned int WINAPI IOCPThreadProc(LPVOID lpParam) { HANDLE hIOCP = *(HANDLE*)lpParam; LPOVERLAPPED lpoverlapped = NULL; LPCLIENT_OVERLAPPED lpoc = NULL; DWORD dwNumbersOfBytesTransfer = 0; ULONG uCompleteKey = 0; while (g_nPorts < 65536) //探测所有的65535个端口号 { int nRet = GetQueuedCompletionStatus(hIOCP, &dwNumbersOfBytesTransfer, &uCompleteKey, &lpoverlapped, INFINITE); lpoc = CONTAINING_RECORD(lpoverlapped, CLIENT_OVERLAPPED, overlapped); switch (lpoc->ulNetworkEvents) { case FD_CONNECT: { int nErrorCode = WSAGetLastError(); if (ERROR_SEM_TIMEOUT != nErrorCode) { printf("当前Ip端口[%d]处于开放状态\n", lpoc->wConnectPort); } lpoc->ulNetworkEvents = FD_CLOSE; shutdown(lpoc->sConnect, SD_BOTH); g_MsSockFunc.DisConnectEx(lpoc->sConnect, lpoverlapped, TF_REUSE_SOCKET, 0); } break; case FD_CLOSE: { InterlockedIncrement(&g_nPorts); //进行原子操作的自增1 lpoc->wConnectPort = g_nPorts; lpoc->ulNetworkEvents = FD_CONNECT; SOCKADDR_IN sockAddr = {0}; sockAddr.sin_addr.s_addr = inet_addr(SERVICE_IP); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(g_nPorts); lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKADDR_IN)); lpoc->dwBufSize = sizeof(SOCKADDR_IN); CopyMemory(lpoc->pBuf, &sockAddr, sizeof(SOCKADDR_IN)); g_MsSockFunc.ConnectEx(lpoc->sConnect, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR), NULL, 0, &lpoc->dwTransBytes, &lpoc->overlapped); } break; default: { lpoc->ulNetworkEvents = FD_CLOSE; HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->pBuf = NULL; lpoc->dwBufSize = 0; g_MsSockFunc.DisConnectEx(lpoc->sConnect, lpoverlapped, TF_REUSE_SOCKET, 0); } } } return 0; }这例子的主要功能是针对具体的IP或者主机名进行TCP的端口探测，这里的端口探测也是采用最简单的方式，向对应的端口发送TCP连接的请求，如果能连上则表示该端口开放，否则认为端口未开放。上述示例中，首先创建IOCP并绑定线程，这里我们绑定处理器数的2倍个线程，并且指定并行的线程数为CPU核数。接着创建对应的结构保存对应的连接信息。然后就是在循环中创建足够数量的SOCKET，这里我们只创建了500个，在每个SOCKET连接完成并回收后再次进行提交去探测后面的端口。注意这里我们先对每个SOCKET进行了绑定，这个在一般的SOCKET客户端服务器模型中没有这个操作，这个操作是WinSock API2.0需要的操作。 创建了足够的socket后，使用ConnectEx进行连接。在线程池中对相关的完成通知进行了处理，这里分了下面几种情况如果是连接成功了，表示这个端口是开放的，这个时候打印对应的端口并进行断开连接和回收SOCKET如果是断开连接的操作完成，再次进行提交如果是其他的通知我们认为是出错了，也就是这个端口没有开放，此时也是回收当前的SOCKET最后当所有端口都探测完成后完成端口线程退出，程序进入资源回收的阶段，这个阶段的顺序如下:关闭线程句柄关闭IOCP句柄关闭监听的SOCKET关闭其余套接字回收其他资源这个顺序也是有一定讲究的，我们先关闭IOCP的相关，如果后续还有需要处理的完成通知，由于此时IOCP已经关闭了，所以这里程序不再处理这些请求，接着关闭监听套接字表示程序已经不再接受连接的请求。这个时候程序已经与服务端彻底断开。后面再清理其余资源。WSABUF 参数在WSASend 和WSARecv的参数中总有一个WSABUF的参数，这个参数很简单的就只有一个缓冲区指针和缓冲区长度，加上函数后面表示WSABUF的个数的参数，很容易想到这些函数可以发送WSABUF的数组，从而可以发送多个数据，但是这就有问题了，发送大数据的话，我们直接申请大一点的缓冲就完了，为什么要额外的定义这么一个结构呢？回答这个问题的关键在于散播和聚集这种I/O处理的机制散播和聚集I/O是一种起源于高级硬盘I/O的技术，它的本质是将一组比较分散的小碎块数据组合成一个大块的IO数据操作，或者反过来是将一个大块的I/O操作拆分为几个小块的I/O操作。它的好处是,比如分散写入不同大小的几个小数据(各自是几个字节),这对于传统硬盘的写入来说是比较麻烦的一种操作,传统的磁盘需要经历几次机械臂的转动寻址,而通过聚集写操作,它会在驱动层将这些小块内存先拼装成一个大块内存,然后只调用一次写入操作,一次性写入硬盘。这样之后,就充分的发挥了高级硬盘系统(DMA/SCSI/RAID等)的连续写入读取的性能优势,而这些设备对于小块数据的读写是没有任何优势的,甚至性能是下降的。而在Winsock中将这种理念发挥到了SOCKET的传输上。WSABUF正是用于这个理念的产物。作为WSASend、WSASendto、WSARecv、WSARecvFrom等函数的数组参数,最终WSABUF数组可以描述多个分散的缓冲块用于收发。在发送的时候底层驱动会将多个WSABUF数据块组合成一个大块的内存缓冲，并一次发送出去，而在接收时会将收到的数据进行拆分，拆分成原来的小块数据，也就是说聚合散播的特性不仅能提高发送的效率，而且支持发送和接收结构化的数据。其实在使用聚合散播的时候主要是应用它来进行数据包的拆分，方便封装自定义协议。在一些应用中,每个数据包都是有自定义的结构的,这些结构就被称为自定义的协议。其中最常见的封装就是一个协议头用以说明包类型和长度,然后是包数据,最后是一个包尾里面存放用于校验数据的CRC码等。但是对于面向伪流的协议来说,这样的结构会带来一个比较头疼的问题——粘包,即多个小的数据包会被连在一起被接收端接收,然后就是头疼和麻烦的拆包过程。而如果使用了散播和聚集I/O方法,那么所有的工作就简单了,可以定义一个3元素的WSABUF结构数组分别发送包头/包数据/包尾。然后接收端先用一个WSABUF接收包头,然后根据包头指出的长度准备包数据/包尾的缓冲,再用2元素的WSABUF接收剩下的数据。同时对于使用了IOCP+重叠I/O的通讯应用来说,在复杂的多线程环境下散播和聚集I/O方法依然可以很可靠的工作。下面是一个使用聚合散播的服务器的例子:#include "MSScokFunc.h" #include <stdio.h> #include "MSScokFunc.h" #include <process.h> typedef struct _tag_CLIENT_OVERLAPPED { OVERLAPPED overlapped; char *pBuf; size_t dwBufSize; DWORD dwFlag; DWORD dwTransBytes; long lNetworkEvents; SOCKET sListen; SOCKET sClient; }CLIENT_OVERLAPPED, *LPCLIENT_OVERLAPPED; unsigned int WINAPI IOCPThreadProc(LPVOID lpParameter) { HANDLE hIOCP = *(HANDLE*)lpParameter; DWORD dwNumberOfTransfered; ULONG uKey = 0; LPOVERLAPPED lpOverlapped = NULL; LPCLIENT_OVERLAPPED lpoc = NULL; BOOL bLoop = TRUE; while (bLoop) { GetQueuedCompletionStatus(hIOCP, &dwNumberOfTransfered, &uKey, &lpOverlapped, INFINITE); lpoc = CONTAINING_RECORD(lpOverlapped, CLIENT_OVERLAPPED, overlapped); switch (lpoc->lNetworkEvents) { case FD_CLOSE: { if (lpoc->sListen == INVALID_SOCKET) { bLoop = FALSE; }else { //再次提交AcceptEx printf("线程(%08x)回收socket(%08x)成功\n", GetCurrentThreadId(), lpoc->sClient); lpoc->dwBufSize = 2 * (sizeof(SOCKADDR_IN) + 16); lpoc->lNetworkEvents = FD_ACCEPT; lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * (sizeof(SOCKADDR_IN) + 16)); g_MsSockFunc.AcceptEx(lpoc->sListen, lpoc->sClient, lpoc->pBuf, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &lpoc->dwTransBytes, &lpoc->overlapped); } } break; case FD_ACCEPT: { SOCKADDR_IN *pRemoteSockAddr = NULL; int nRemoteSockAddrLength = 0; SOCKADDR_IN *pLocalSockAddr = NULL; int nLocalSockAddrLength = 0; g_MsSockFunc.GetAcceptExSockAddrs(lpoc->pBuf, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, (LPSOCKADDR*)&pLocalSockAddr, &nRemoteSockAddrLength, (LPSOCKADDR*)&pRemoteSockAddr, &nRemoteSockAddrLength); printf("有客户端[%s:%d]连接进来，当前通信地址[%s:%d]......\n", inet_ntoa(pRemoteSockAddr->sin_addr), ntohs(pRemoteSockAddr->sin_port), inet_ntoa(pLocalSockAddr->sin_addr), ntohs(pLocalSockAddr->sin_port)); //设置当前通信用socket继承监听socket的相关属性 int nRet = ::setsockopt(lpoc->sClient, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char *)&lpoc->sListen, sizeof(SOCKET)); //投递WSARecv消息, 这里仅仅是为了发起WSARecv调用而不接受数据，设置缓冲为0可以节约内存 WSABUF buf = {0, NULL}; HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->lNetworkEvents = FD_READ; lpoc->pBuf = NULL; lpoc->dwBufSize = 0; WSARecv(lpoc->sClient, &buf, 1, &lpoc->dwTransBytes, &lpoc->dwFlag, &lpoc->overlapped, NULL); } break; case FD_READ: { WSABUF buf[2] = {0}; DWORD dwBufLen = 0; buf[0].buf = (char*)&dwBufLen; buf[0].len = sizeof(DWORD); //当调用此处的时候已经完成了接收数据的操作，此时只要调用WSARecv将数据放入指定内存即可,这个时候不需要使用重叠IO操作了 int nRet = WSARecv(lpoc->sClient, buf, 1, &lpoc->dwTransBytes, &lpoc->dwFlag, NULL, NULL); DWORD dwBufSize = dwBufLen; buf[1].buf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufSize); buf[1].len = dwBufLen; WSARecv(lpoc->sClient, &buf[1], 1, &lpoc->dwTransBytes, &lpoc->dwFlag, NULL, NULL); printf("client>%s\n", buf[1].buf); lpoc->pBuf = buf[1].buf; //这块内存将在FD_WRITE事件中释放 lpoc->dwBufSize = buf[1].len; lpoc->lNetworkEvents = FD_WRITE; lpoc->dwFlag = 0; WSASend(lpoc->sClient, buf, 2, &lpoc->dwTransBytes, lpoc->dwFlag, &lpoc->overlapped, NULL); } break; case FD_WRITE: { printf("线程[%08x]完成事件(WSASend),缓冲(%d),长度(%d), 发送长度(%d)\n", GetCurrentThreadId(), lpoc->pBuf, lpoc->dwTransBytes, dwNumberOfTransfered); HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->dwFlag = 0; lpoc->lNetworkEvents = FD_CLOSE; lpoc->pBuf = NULL; shutdown(lpoc->sClient, SD_BOTH); g_MsSockFunc.DisConnectEx(lpoc->sClient, &lpoc->overlapped, TF_REUSE_SOCKET, 0); } break; } } printf("线程[%08x] 退出.......\n", GetCurrentThreadId()); return 0; }这里没有展示main函数的内容，main函数的内容与之前的相似，在开始的时候进行一些初始化，在结束的时候进行资源的回收操作。这里需要注意的是在main函数中给定的退出条件:CLIENT_OVERLAPPED CloseOverlapped = {0}; CloseOverlapped.lNetworkEvents = FD_CLOSE; CloseOverlapped.sListen = INVALID_SOCKET; for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { PostQueuedCompletionStatus(hIOCP, 0, NULL, (LPOVERLAPPED)&CloseOverlapped.overlapped); }在线程中，首先判断当前完成事件的类型如果是 FD_CLOSE 事件，首先判断socket是否为 INVALID_SOCKET，如果是则表明是主程序需要退出线程，此时退出，否则只进行回收与再提交的操作如果是 FD_ACCEPT 事件，则表明有客户端连接上来，此时解析客户端的信息并提交WSARecv的信息，注意这里在提交WSARecv时给的缓冲是NULL,这里表示我只需要一个完成的通知，在完成收到客户端数据后会触发FD_READ,而不需要进行数据的写入，一般在提交WSARecv后，系统内核会锁住我们提供的WSABUF结构所表示的那块内存直到，写入完成，而从我们提交WSARecv到真正获取到客户端传过来的数据，这个是需要一定时间的，而在这个时间中虽然CPU不会等待，但是这个内存被占用了，我们认为这也是一种浪费。特别是在服务端需要频繁的调用WSASend、WSARecv这样的操作，如果每一个都锁定一定的缓冲，这个内存消耗也是惊人的。所以这里传入NULL,只让其进行事件通知，而写入的操作由程序自己做。当事件是FD_READ时才正式进行数据的写入操作，此时再次调用WSARecv，这个时候需要注意，我们已经接收到了客户端的数据完成通知，也就是说现在明确的知道客户端已经发送了数据，而且内核已经收到数据，此时就不需要再使用异步了，只需要使用同步简单的读取数据即可。 在读取数据的时候首先根据WSABUF数组的第一个元素获取数据包的长度，然后分配对应的缓冲，接着接收后面真实的数据包。最后调用WSASend将数据原样返回。当完成通知事件是 FD_WRITE时表示我们已经完成了发送数据到客户端的操作，此时断开与客户端的连接并清理对应的缓冲。提高服务程序性能的一般方式对于实际的面向网络的服务(主要指使用TCP协议的服务应用)来说,大致可以分为两大类:连接密集型/传输密集型。连接密集型服务的主要设计目标就是以最大的性能响应尽可能多的客户端连接请求,比如一个Web服务传输密集型服务的设计目标就是针对每个已有连接做到尽可能大的数据传输量,有时以牺牲连接数为代价,比如一个FTP服务器。而针对像UDP这样无连接的协议，我们认为它主要负责传输数据，所以这里把它归结为传输密集型对于面向连接的服务(主要指使用TCP协议的服务)来说,主要设计考虑的是如下几个环节:接受连接数据传输IOCP+线程池接力其它性能优化考虑接收连接接收连接一般都采用AcceptEx的重叠IO方式来进行等待，并且一般需要加上SOCKET池的机制，开始时可能准备的AcceptEx数量可能会不足,此时可以另起线程在监听SOCKET句柄上等待FD_ACCEPT事件来决定何时再次投递大量的AcceptEx进行等待当然再次调用AcceptEx时需要创建大量的SOCKET句柄,这个工作最好不要在IOCP线程池线程中进行,以防创建过程耗时而造成现有SOCKET服务响应性能下降。最终需要注意的就是,任何处于"等待"AcceptEx状态的SOCKET句柄都不要直接调用closesocket,这样会造成严重的内核内存泄漏。应该先关闭监听套接字，防止在关闭SOCKET的时候有客户端连接进来，然后再调用closesocket来断开。数据传输在这个环节中可以将SOCKET句柄上的接收和发送数据缓冲区设置为0，这样可以节约系统内核的缓冲,尤其是在管理大量连接SOCKET句柄的服务中,因为一个句柄上这个缓冲大小约为17K,当SOCKET句柄数量巨大时这个缓冲耗费还是惊人的。设置缓冲为0的方法如下:int iBufLen= 0; setsockopt(skAccept,SOL_SOCKET,SO_SNDBUF,(const char*)&iBufLen,sizeof(int)); setsockopt(skAccept,SOL_SOCKET,SO_RCVBUF,(const char*)&iBufLen,sizeof(int));IOCP + 线程池为了性能的考虑,一般网络服务应用都使用IOCP+重叠I/O+SOCKET池的方式来实现具体的服务应用。这其中需要注意的一个问题就是IOCP线程池中的线程不要用于过于耗时或复杂的操作,比如:访问数据库操作,存取文件操作,复杂的数据计算操作等。这些操作因为会严重占用SOCKET操作的IOCP线程资源因此会极大降低服务应用的响应性能。但是很多实际的应用中确实需要在服务端进行这些操作,那么如何来平衡这个问题呢? 这时就需要另起线程或线程池来接力IOCP线程池的工作。比如在WSARecv的完成通知中,将接收到的缓冲直接传递给QueueUserWorkItem线程池方法,启动线程池中的线程去处理数据,而IOCP线程池则继续专心于网络服务。这也就是一般书上都会说的专门的线程干专门的事其他的性能考虑其他的性能主要是使用之前提到的聚合与散播机制，或者对函数和代码执行流程进行优化关于IOCP 聚合与散播的代码全放在码云上了: 示例代码
WinSock中提供的5种网络模型已经可以做到很高效了，特别是完成端口，它的高效的原因在于它不仅另外开启了线程来处理完成通知而不是占用主程序的时间，同时也在于我们在完成端口中运用了大量异步IO处理函数。比如WSASend、WSARecv等等。为了高效的处理网络IO，WinSock提供了大量这样的异步函数。这篇博文主要探讨这些函数的用法和他们与传统的巴克利套接字相比更加高效的秘密AcceptEx其实在使用TCP协议编程时，接受连接的过程也是需要进行收发包操作的，具体的过程请参考TCP的三次握手。针对这种特性WinSock提供了对应的异步操作函数AcceptEx。函数原型如下:BOOL AcceptEx( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );sListenSocket: 监听套接字sAcceptSocket：该参数是一个SOCKET的句柄，一旦连接成功建立，那么会使用该SOCKET作为通信的SOCKETlpOutputBuffer：是三个数据一体化的缓冲区的指针，这三个数据分别是接收连接时顺带接收客户端发过来的数据的缓冲，之后是本地地址结构的缓冲，最后是远程客户端地址结构的指针dwReceiveDataLength：是lpOutputBuffer的缓冲长度dwLocalAddressLength：是本地地址结构长度,其值等于sizeof(SOCKADDR)+16dwRemoteAddressLength：是远程客户端地址结构长度,其值也等于sizeof(SOCKADDR)+16lpdwBytesReceived：该参数用于返回接受连接请求时接收的数据的长度lpOverlapped：就是重叠I/O需要的结构第一个参数是一个十分重要的参数，这个参数是AcceptEx比较高效的一个重要的原因。从功能上来看它与传统的accept函数并没有什么区别，都是接受客户端连接的。它与accpet相比比较高效的原因如下：从内部机理来说accpet在内部其实有一个创建SOCKET的操作，当函数成功后会返回这个SOCKET，所以AcceptEx与accept相比少了一个创建SOCKET的操作，它的功能更加纯粹，这就给了我们一个启示：我们可以在初始化的时候创建大量的SOCKET，并投递到AcceptEx中，这样在接受连接时省去了创建SOCKET的时间，能够更快速的响应客户端的连接。由于AcceptEx不用创建SOCKET，所以它也将accept内部对socket设置的操作给省去了，也少了一些其他的附带操作，比如地址的解析，其实这里我们可以简单的理解为lpOutputBuffer中保存的信息就是TCP三次握手中的SYN包和ACK包，这些包的信息需要在函数返回后由用户通过其他方法来解析，而accpet帮我们解析了，所以AcceptEx比accept更加高效因为AcceptEx的设计目标纯粹就是为了性能,所以监听套接字的属性不会被代表客户端通讯的套接字自动继承。要继承属性(包括socket内部接受/发送缓存大小等等)就必须调用setsockopt使用选项SO_UPDATE_ACCEPT_CONTEXT,如下:int nRet = ::setsockopt(skClient, SOL_SOCKET ,SO_UPDATE_ACCEPT_CONTEXT ,(char *)&skListen, sizeof(skListen));AcceptEx函数除了能够接受客户端的连接之外，它也可以在接受连接的同时接收客户端随着连接请求一块发过来的数据，只要我们设置dwReceiveDataLength 参数大于0，并在lpOutputBuffer中分配相应的缓冲即可，但是这里会存在一个安全问题，当我们设置了这些之后，如果客户端只发送连接请求，但是不发送数据，AcceptEx会一直等待，如果有大量这样的客户端，那么可能会给服务器造成大量的资源浪费从而不能及时的服务其他正常客户端。要防止这样的情况，可以采用下列措施：设置dwReceiveDataLength为0，并且不分配对应的缓冲，也就是关闭这个接收数据的功能。启动一个监视线程对用于连接的SOCKET轮询调用：int iSecs; int iBytes = sizeof( int ); getsockopt( hAcceptSocket, SOL_SOCKET, SO_CONNECT_TIME, (char *)&iSecs, &iBytes ); //获取SOCKET连接时间iSecs 为 -1 表示还未建立连接, 否则就是已经连接的时间.当iSecs超过某个筏值时,就果断断开这个连接GetAcceptExSockAddr前面说AcceptEx不会对地址进行解析，而是返回一个经过编码的地址信息，可以将它理解为原始的三次握手包。而函数GetAcceptExSockAddr的主要作用就是通过原始的二进制数据得到对应的地址结构。函数原型如下:void GetAcceptExSockaddrs( PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPSOCKADDR* LocalSockaddr, LPINT LocalSockaddrLength, LPSOCKADDR* RemoteSockaddr, LPINT RemoteSockaddrLength);lpOutputBuffer:之前提供给AcceptEx函数的缓冲，如果AcceptEx调用成功，会在这个缓冲中写入地址信息，GetAcceptExSockaddrs通过这个缓冲中保存的地址信息来解析出地址结构dwReceiveDataLength：接收到的数据长度，注意这个长度不是lpOutputBuffer，而是客户端随着连接请求一起发送过来的其他数据的长度，其实这里应该理解为地址信息在缓冲中的偏移dwLocalAddressLength：本地地址信息的长度，这个长度为sizeof(SOCKADDR)+16dwRemoteAddressLength：远程客户端的地址信息的长度，长度为sizeof(SOCKADDR)+16LocalSockaddr：解析出来的本地地址结构LocalSockaddrLength:本地地址结构的长度，这个参数是一个输出参数RemoteSockaddr: 解析出来的远程客户端的地址结构RemoteSockaddrLength：解析出来的远程客户端的地址长度，这个参数是一个输出参数这里为什么要返回本地的地址结构呢，主要有两个原因：一般的服务器可能有多块网卡，返回本地地址我们就可以知道服务器用哪块网卡与客户端通信服务器用来监听的端口与用来进行通信的端口不是同一个，返回本地地址我们就能够知道服务器在使用哪个端口与客户端通信TransmitFile对于一些网络应用来说,发送文件有时是一个基本的功能,比如:web服务,FTP服务等。在Winsock中为此而专门提供了一个高效传输文件的API——TransmitFile。函数原型如下:BOOL TransmitFile( SOCKET hSocket, HANDLE hFile, DWORD nNumberOfBytesToWrite, DWORD nNumberOfBytesPerSend, LPOVERLAPPED lpOverlapped, LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, DWORD dwFlags);这个函数主要工作在TCP协议上hSocket：与客户端进行通信的SOCKEThFile：是对应文件的句柄nNumberOfBytesToWrite：表示发送文件的长度，这个长度可以小于文件长度nNumberOfBytesPerSend：当文件较大时，可以进行拆包发送，这个参数表示每个数据包的大小，如果这个参数为0，将采用系统默认的包大小，NT内核中默认大小为64KlpOverlapped：重叠I/O需要的结构lpTransmitBuffers：是一个TRANSMIT_FILE_BUFFERS结构体,利用它可以指定在文件开始发送前需要发送的额外数据以及文件发送结束后需要发送的额外数据,这个参数也可以置为NULL,仅表示发送文件数据。它的结构如下所示:typedef struct _TRANSMIT_FILE_BUFFERS { PVOID Head; DWORD HeadLength; PVOID Tail; DWORD TailLength; } TRANSMIT_FILE_BUFFERS;dwFlags：它是一个按位组合的标识。它的各个标识的含义如下标识含义TF_DISCONNECT在传输文件结束后,开始一个传输层断开动作TF_REUSE_SOCKET重置套接字,使其可以被AcceptEx等函数重用,这个标志需要与TF_DISCONNECT标志合用TF_USE_DEFAULT_WORKER指定发送文件使用系统默认线程,这对传输大型文件很有利TF_USE_SYSTEM_THREAD使用系统线程发送文件，它与TF_USE_DEFAULT_WORKER作用相同TF_USE_KERNEL_APC指定利用内核APC队列来代替工作线程来处理文件传输. 需要注意的是系统内核APC队列只在应用程序进入等待状态时才工作. 但不一定非要一个可警告状态的等待TF_WRITE_BEHIND指定TransmitFile函数尽可能立即返回,而不管远端是否确认已收到数据.这个标志不能与TF_DISCONNECT和TF_REUSE_SOCKET一起使用可以使用TF_DISCONNECT加上TF_REUSE_SOCKET 来回收SOCKET，以便像AcceptEx这样的函数可以重新利用。此时应该指定hFile为NULL，但这不是这个函数的主业(我觉得应该让专门的函数干专门的事，自己在封装函数的时候也应该要注意，不要向Win32 API这样使用各种标志来控制函数的功能)同时TransmitFile函数只有在服务器版Windows上才能发挥其全部功能。而在专业版或家庭版等Windows上它被限定为最多同时有两个调用在传输,而其他的调用都被置为排队等待状态。发送文件这个功能，是一个十分简单的功能，无非是应用层不断从磁盘文件中读取文件并使用WSASend这样的异步函数来发送，另一端不断用WSARecv接收并写入到文件中，为了性能在读写文件时也可以用IOCP的方式，那么为什么微软为了这么一个简单的功能还要独自封装一个函数，难道它封装的函数就一定比我们自己实现的性能高？上图揭示了TransmitFile能够高效工作的秘密，一般我们来封装这个功能的时候会调用ReadFile，此时由内核层读取文件并将文件文件内容保存在内核的内存空间中，然后通过系统调用们将内容拷贝到R3层，在调用WSASend的时候会将文件内容再从R3层拷贝到R0层，这个过程经过系统调用们，需要调用各种函数，并且进行各种验证。这个操作是十分耗时的。而TransmitFile则相对要高效的多，既然最终是要发送文件，那么它将内容从文件中读取出来后直接将R0层中保存的文件内容通过SOCKET发送出去，有的时候直接采用文件映射的方式将磁盘地址映射到网卡中，直接由网卡读取并发送，这样又省去了从内核中读取文件并拷贝到网卡缓存中的操作。所以它比我们自己封装来的更加高效。TransmitPackets有的时候需要发送超大型数据(有时是几十G)到客户端,有时甚至需要发送多个文件到客户端。这个时候TransmitFile就不再有效了。请注意TransmitFile的第三个参数 nNumberOfBytesToWrite 是一个DWORD类型，这也就标明这个函数最大只能发送4GB的文件，而对于更大的文件它就无能为力了，为了发送更大的文件WinSock专门封装了一个函数——TransmitPacketsBOOL PASCAL TransmitPackets( SOCKET hSocket, LPTRANSMIT_PACKETS_ELEMENT lpPacketArray, DWORD nElementCount, DWORD nSendSize, LPOVERLAPPED lpOverlapped, DWORD dwFlags );这个函数不但可以在面向连接(面向流)式的协议(TCP)上工作,还可以在无连接式的数据报协议(UDP)上工作,而TransmitFile函数只能工作在TCP上hSocket：表示发送所用的SOCKETlpPacketArray：它是一个结构体数组的指针，这个结构体表示发送文件的相关信息，结构体的定义如下:typedef struct _TRANSMIT_PACKETS_ELEMENT { ULONG dwElFlags; ULONG cLength; union { struct { LARGE_INTEGER nFileOffset; HANDLE hFile; }; PVOID pBuffer; }; } TRANSMIT_PACKETS_ELEMENT;这个结构体主要包含3个部分，第一个部分是一个标志，表示该如何解释后面的部分，这个标志有如下几个值标志含义TP_ELEMENT_FILE标明它将发送一个文件，此时会使用共用体中的结构体部分TP_ELEMENT_MEMORY标明它将要将发送内存中的一段空间的数据，此时会使用共用体中pBuffer部分TP_ELEMENT_EOP而最后一个标志用于辅助说明前两个标志,说明当前结构表示的数据应当作为一个结束包来发送,也就说之前所有的数据到当前这个结构描述的数据应当视为一个包第二部分是cLength用以说明当前结构描述的数据长度/发送文件内容的长度第三个部分联合定义根据第一个部分的实际标志值,用于说明是一个文件还是一个内存块,当是一个文件时还可以指定一个64位长整数型的文件内偏移,这为应用利用TransmitPackets发送大于4GB的文件创造了可能.当偏移为-1时,表示从文件当前指针位置开始发送需要注意的是因为TransmitPackets能够很快的处理数据发送,因此可能会造成大量待发送数据堆积在下层协议的协议栈上.而对于无连接的面向数据报的协议来说,有时协议驱动会选择将它们简单丢弃.另外对于TransmitPackets来说也只有服务器版的Windows能够发挥它全部的性能,而对于家庭版和专业版来说,最多能够同时处理两个TransmitPackets调用,其它的调用都会被排队处理最后TransmitPackets在发送文件时工作机理与TransmitFile是类似的,而TransmitPackets可以发送多个文件,并且可以发送超大文件(大于4GB),在发送内存块上,TransmitPackets也有很多优化,调用者可以放心的将超大的缓冲块传递给TransmitPackets而不必过多的担心ConnectEx作为客户端应用来说,或者说一些需要反连接工作的应用来说(如:Active FTP方式的服务器),使用传统的connect进行阻塞式或非阻塞式的编程都无法得到很好的性能响应它的定义如下:BOOL PASCAL ConnectEx( SOCKET s, const struct sockaddr* name, int namelen, PVOID lpSendBuffer, DWORD dwSendDataLength, LPDWORD lpdwBytesSent, LPOVERLAPPED lpOverlapped );s: 进行连接操作的SOCKET句柄,这个SOCKET句柄需要事先绑定，这里与调用普通的connect函数不同，它需要先调用bind函数将本地地址与SOCKET绑定name：要连接的远端服务器的地址结构namelen：就是远端地址结构的长度lpSendBuffer,dwSendDataLength,lpdwBytesSent三个参数共同用于描述在连接到服务器成功之后向服务器直接发送的数据缓冲,长度以及实际发送的数据长度lpOverlapped就是重叠I/O操作需要的结构体与AcceptEx类似，在连接成功后，需要调用 setsocketopt 来设置SOCKET的属性。与传统的connect函数不同,ConnectEx函数要求一个已经绑定过的SOCKET句柄参数,其实这也是将connect内部的绑定操作排除在真正connect操作之外的一种策略。最终连接的操作也会很快的就被完成,而绑定可以提前甚至在初始化的时候就完成。这样做也是为了能够快速的处理网络事件。DisConnectEx前面在TransmitFile中说它可以使用TF_DISCONNECT加上TF_REUSE_SOCKET 来回收SOCKET，也提到应该用专门的函数来干专门的事，这里ConnectEx就是专门的函数。它主要的作用与普通的closesocket函数类似。BOOL DisconnectEx( SOCKET hSocket, LPOVERLAPPED lpOverlapped, DWORD dwFlags, DWORD reserved );hSocket ：表示将要被回收的SOCKETlpOverlapped：重叠IO所使用的结构dwFlags：它是一个标志值，表示是否需要回收SOCKET，如果为0则表示不需要回收，此时它的作用与closesocket类似。如果为TF_REUSE_SOCKET表示将回收SOCKETreserved：是一个保留值直接传0即可当以重叠I/O的方式调用DisconnectEx时,若该SOCKET还有未完成的传输调用时,该函数会返回FALSE,并且最终错误码是WSA_IO_PENDING,即断开/回收操作将在传输完成后执行。如果使用了重叠IO，同样在完成之后会调用完成处理函数。如果未采用重叠IO操作，那么函数会阻塞，直到数据发送完成并断开连接。扩展函数的动态加载之前介绍的这一系列Winsock2.0的扩展API,最好都动态加载之后再行调用,因为它们具体的导出位置在不同平台上变动太大,如果静态联编的话,会给开发编译工作带来巨大的麻烦,所以使用运行时动态加载来调用这些API。但是这些函数的加载与加载普通的dll函数不同，为了方便操作，WinSock提供了一套完整的支持。这表示我们不需要知道它们所在的dll，我们可以直接使用WinSock提供的方法，即使以后它们所在的dll文件变化了，也不会影响我们的使用。加载它们需要使用到函数WSAIoctl,函数原型如下:int WSAIoctl( SOCKET s, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cbInBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbBytesReturned, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );这个函数的使用方法与ioctlsocket 相似。这里不对它的详细用法进行讨论。这里就简单的说说该怎么用它加载这些函数。要加载WinSock API，首先需要将第二个控制码参数设置为SIO_GET_EXTENSION_FUNCTION_POINTER，表示获取扩展API的指针。设置了这个参数后，lpvInBuffer参数需要设置成相应函数的GUID，下面列举了各个函数的GUID值GDUI函数WSAID_ACCEPTEXAcceptExWSAID_CONNECTEXConnectExWSAID_DISCONNECTEXDisconnectExWSAID_GETACCEPTEXSOCKADDRSGetAcceptExSockaddrsWSAID_TRANSMITFILETransmitFileWSAID_TRANSMITPACKETSTransmitPacketsWSAID_WSARECVMSGWSARecvMsgWSAID_WSASENDMSGWSASendMsg函数的指针通过 lpvOutBuffer 参数返回，而cbOutBuffer表示接受缓冲的长度，lpcbBytesReturned表示返回数据的长度。后面两个参数都与完成IO有关，所以这里可以直接给NULL。下面是一个加载AcceptEx函数的例子typedef BOOL (PASCAL FAR * LPFN_ACCEPTEX)( IN SOCKET sListenSocket, IN SOCKET sAcceptSocket, IN PVOID lpOutputBuffer, IN DWORD dwReceiveDataLength, IN DWORD dwLocalAddressLength, IN DWORD dwRemoteAddressLength, OUT LPDWORD lpdwBytesReceived, IN LPOVERLAPPED lpOverlapped ); LPFN_ACCEPTEX pFun = NULL; SOCKET sTemp = WSASocket(af, type, protocol, NULL, NULL, 0); GUID funGuid = WSAID_ACCEPTEX; if (INVALID_SOCKET != skTemp) { DWORD dwOutBufferSize = 0; int Ret = ::WSAIoctl(skTemp, SIO_GET_EXTENSION_FUNCTION_POINTER, &funGuid, sizeof(funGuid), &pFun, sizeof(pFun), &dwOutBufferSize, NULL, NULL); } 这里调用WSAIoctl加载扩展函数时需要传入SOCKET句柄，它其实是利用传入的SOCKET的相关信息来导出对应版本的扩展函数，比如这里我们传入的是一个用在TCP协议之上的SOCKET，所以它会返回一个使用TCP协议的API，利用这个SOCKET，这个函数以及它返回的API真正做到了与协议无关。
title: WinSock 完成端口模型tags: [WinSock 模型, 网络编程, 完成端口]date: 2018-07-06 20:44:39categories: Windows 网络编程keywords: WinSock 模型, 网络编程, 完成端口之前写了关于Winsock的重叠IO模型，按理来说重叠IO模型与之前的模型相比，它的socket即是非阻塞的，也是异步的，它基本上性能非常高，但是它主要的缺点在于，即使我们使用历程来处理完成通知，但是我们知道历程它本身是在对应线程暂停，它借用当前线程的线程环境来执行完成通知，也就是说要执行完成通知就必须暂停当前线程的工作。这对工作线程来说也是一个不必要的性能浪费，这样我们自然就会想到，另外开辟一个线程来执行完成通知，而本来的线程就不需要暂停，而是一直执行它自身的任务。处于这个思想，WinSock提供了一个新的模型——完成端口模型。完成端口简介完成端口本质上是一个线程池的模型，它需要我们创建对应的线程放在那，当完成通知到来时，他会直接执行线程。在这5中模型中它的性能是最高的。在文件中我们也提到过完成端口，其实我们利用Linux上一切皆文件的思想来考虑这个问题就可以很方便的理解，既然我们需要异步的方式来读写网卡的信息，这与读写文件的方式类似，既然文件中存在完成端口模型，网络上存在也就不足为奇了。对于完成端口Windows没有引入新的API函数，而是仍然采用文件中一堆相关的函数。可以使用CreateIoCompletionPort来创建完成端口的句柄，该函数原型如下:HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in_opt HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads );第一个参数是与完成端口绑定的文件句柄，如果我们要创建完成端口句柄，这个值必须传入INVALID_HANDLE_VALUE。如果是要将文件句柄与完成端口绑定，这个参数必须穿入一个支持完成端口的文件句柄。在Winsock中如果要绑定SOCKET到完成端口只需要将SOCKET强转为HANDLE。第二个参数是一个已知的完成端口句柄，如果是创建完成端口，这个参数填入NULL。第三个参数是一个LONG型的指针，它作为一个标志，由完成通知传入完成线程中，用来标识不同的完成通知。一般我们会定义一个扩展来OVERLAPPED结构来标识不同的完成通知，所以这个参数一般不用传入NULL。第四个参数是同时执行的线程数，如果是绑定文件句柄到完成端口，则这个参数填入0我们可以在对应的完成线程中调用GetQueuedCompletionStatus函数来获取完成通知，这个函数只有当有IO操作完成时才会返回，函数原型如下:BOOL WINAPI GetQueuedCompletionStatus( __in HANDLE CompletionPort, __out LPDWORD lpNumberOfBytes, __out PULONG_PTR lpCompletionKey, __out LPOVERLAPPED* lpOverlapped, __in DWORD dwMilliseconds ); 它的第一个参数是一个完成端口的句柄。第二个参数表示当前有多少字节的数据完成IO操作。第三个参数是一个标记值，用来标识不同文件句柄对应的完成通知，它是通过 CreateIoCompletionPort 函数设置的那个标识。第四个参数是OVERLAPPED结构。第五个参数表示等待的时间，如果填入INFINITE则会一直等到有IO操作完成。完成端口的示例：下面是一个完成端口的示例typedef struct _tag_MY_OVERLAPPED { OVERLAPPED m_overlapped; SOCKET m_sClient; long m_lEvent; DWORD m_dwNumberOfBytesRecv; DWORD m_dwFlags; char *m_pszBuf; LONG m_dwBufSize; }MY_OVERLAPPED, *LPMY_OVERLAPPED; unsigned int __stdcall IOCPThread(LPVOID lpParameter); #define BUFFER_SIZE 1024 #define SERVER_PORT 6000 int _tmain(int argc, TCHAR *argv) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SYSTEM_INFO si = {0}; GetSystemInfo(&si); //创建完成端口对象 HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, si.dwNumberOfProcessors); //创建完成端口对应的线程对象 HANDLE *pThreadArray = (HANDLE *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * si.dwNumberOfProcessors); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { pThreadArray[i] = (HANDLE)_beginthreadex(NULL, 0, IOCPThread, &hIocp, 0, NULL); } SOCKET SrvSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); SOCKADDR_IN SockAddr = {0}; SockAddr.sin_family = AF_INET; SockAddr.sin_port = htons(SERVER_PORT); SockAddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(SrvSocket, (SOCKADDR*)&SockAddr, sizeof(SOCKADDR)); listen(SrvSocket, 5); SOCKET sClient = accept(SrvSocket, NULL, NULL); CreateIoCompletionPort((HANDLE)sClient, hIocp, NULL, 0); WSABUF buf = {0}; buf.buf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, BUFFER_SIZE); buf.len = BUFFER_SIZE; MY_OVERLAPPED AcceptOverlapped = {0}; AcceptOverlapped.m_dwBufSize = BUFFER_SIZE; AcceptOverlapped.m_lEvent = FD_READ; AcceptOverlapped.m_pszBuf = buf.buf; AcceptOverlapped.m_sClient = sClient; WSARecv(sClient, &buf, 1, &AcceptOverlapped.m_dwNumberOfBytesRecv, &AcceptOverlapped.m_dwFlags, &AcceptOverlapped.m_overlapped, NULL); while (TRUE) { int nVirtKey = GetAsyncKeyState(VK_ESCAPE); //用户按下退出键(ESC) { break; } } for (int i = 0; i < si.dwNumberOfProcessors * 2; i++) { //向IOCP发送FD_CLOSE消息，以便对应线程退出 AcceptOverlapped.m_lEvent = FD_CLOSE; PostQueuedCompletionStatus(hIocp, si.dwNumberOfProcessors * 2, 0, &AcceptOverlapped.m_overlapped); } WaitForMultipleObjects(2 * si.dwNumberOfProcessors, pThreadArray, TRUE, INFINITE); for (int i = 0; i < si.dwNumberOfProcessors * 2; i++) { CloseHandle(pThreadArray[i]); } HeapFree(GetProcessHeap(), 0, buf.buf); shutdown(sClient, SD_BOTH); closesocket(sClient); CloseHandle(hIocp); WSACleanup(); return 0; } unsigned int __stdcall IOCPThread(LPVOID lpParameter) { HANDLE hIocp = *(HANDLE*)lpParameter; DWORD dwNumberOfBytes = 0; MY_OVERLAPPED *lpOverlapped = NULL; ULONG key = 0; BOOL bLoop = TRUE; while (bLoop) { BOOL bRet = GetQueuedCompletionStatus(hIocp, &dwNumberOfBytes, &key, (LPOVERLAPPED*)&lpOverlapped, INFINITE); if (!bRet) { continue; } switch (lpOverlapped->m_lEvent) { case FD_CLOSE: //退出 { bLoop = FALSE; printf("线程[%08x]准备退出......\n", GetCurrentThreadId()); } break; case FD_WRITE: { printf("数据发送完成......\n"); shutdown(lpOverlapped->m_sClient, SD_BOTH); closesocket(lpOverlapped->m_sClient); } break; case FD_READ: { printf("client>%s", lpOverlapped->m_pszBuf); lpOverlapped->m_lEvent = FD_WRITE; WSABUF buf = {0}; buf.buf = lpOverlapped->m_pszBuf; buf.len = dwNumberOfBytes; lpOverlapped->m_dwFlags = 0; WSASend(lpOverlapped->m_sClient, &buf, 1, &lpOverlapped->m_dwNumberOfBytesRecv, lpOverlapped->m_dwFlags, &lpOverlapped->m_overlapped, NULL); } } } return 0; }在上述代码中，首先定义了一个结构体用来保存额外的数据。在main函数中首先查询CPU的核数，然后创建这个数目2倍的线程。接着创建一个完成端口对象。然后进行SOCKET的创建、绑定、监听、接收连接的操作。当有连接进来的时候。创建对应的扩展结构并调用WSARecv投递一个接收操作。由于后面的收发操作都在对应的线程中操作，因此在主线程中只需要等待即可。当用户确定退出时。先调用PostQueuedCompletionStatus函数向完成线程中发送完成通知，并将网络事件设置为FD_CLOSE，表示让线程退出。在这里没有使用TerminateThread这种暴力的方式，而选择了一种让线程自动退出的温和的方式。接着进行资源的回收，最后退出。在线程中，我们首先在循环中调用 GetQueuedCompletionStatus函数来获取完成通知，当发生完成事件时，我们在switch中根据不同的额网络事件来处理，针对FD_CLOSE事件，直接退出线程。针对FD_READ事件，先打印客户端发送的信息，然后调用WSASend将信息原样返回，接着设置网络事件为FD_WRITE，以便断开与客户端的链接。几种模型的比较最后针对5种模型和两种socket工作模式来做一个归纳说明。最先学习的是SOCKET的阻塞模式，它的效率最低，它会一直等待有客户端连接或者有数据发送过来才会返回。这就好像我们在等某个人的信，但是不知道这封信什么时候能送到，于是我们在自家门口的收信箱前一直等待，直到有信到来。为了解决这个问题，提出了SOCKET的非阻塞模式，它不会等待连接或者收发数据的操作完成，当我们调用对应的accept或者send、recv时会立即返回，但是我们不知道它什么时候有数据要处理，如果针对每个socket都等待直到有数据到来，那么跟之前的阻塞模式相比没有任何改进，于是就有了socket模式，它会等待多个socket，只要其中有一个有数据就返回，并处理。用收信的模型类比的话，现在我们不用在邮箱前等待了。但是我们会每隔一段时间就去邮箱那看看，有没有信，有信就将它收回否则空手而归。我们说select模型的最大问题在于不知道什么时候有待决的SOCKET，因此我们需要在循环中不停的等待。为了解决这个时机问题，又提出了WSAAsyncSelect模型和WSAEvent模型，它们主要用来解决调用对应函数的时机。用收信的例子类比就是现在我在邮箱上装了一个报警的按钮，只有有信，警报就会响，这个时候我们就去收信。而不用向之前那样每隔一段时间就去邮箱看看我们说解决了时机的问题，但是调用send和recv对网卡进行读写操作仍然是同步的操作，CPU需要傻傻的等着数据从网卡读到内存或者从内存写到网卡上。因此又有了重叠IO的模型和一些列的新的API，向WSARecv和WSASend等等函数。这样就相当于当有信来的警报响起时，我们不需要自己去取信了，另外派了一个人帮我们拿信，这样我们的工作效率又提高了一些。节约了我们的时间重叠IO也有它的问题，如果使用重叠IO的事件模型时，也需要在合适的时候等待，就好像我们虽然派了一个人来帮忙拿信，但是我们自己却需要停下手头上的工作，询问拿信的人回来了。而使用完成历程也存在自己的问题，因为它需要使用主线程的资源来执行历程，它需要主线程暂停下来，这样就可能出现两种情况：1）有通知事件到来，但是并没有进入可警告状态；2）进入可警告状态却没有客户端发送请求。这就相当于可能我们不停的等待但是拿信的那个人却没有回来，或者拿信的人回来了，我们却没有时间处理信件。针对重叠IO的上述问题，提出了完成端口的解决方案，完成事件由对应的线程处理，而主线程只需要专注于它自己的工作就好了，这就相当于警报响了，我们知道信来了，直接派一个人去拿信，后面的我就不管了，而拿信的人把信拿回来的时候将信放好。当我们忙完之后去处理这封信。没忙完的话信就一直放在那，甚至让拿信的人处理这封信，这样就能更高效的集中注意力来处理眼前的工作。