Masimaro's blog - Windows 编程
type: channel
link: https://www.masimaro.top/category/windows_program/
description: Windows平台相关编程，VC++，QT在Windows平台的相关问题
items: 记一次内存泄露调试
items: VC+++ 操作word
items: 使用miniblink 在程序中嵌入浏览器
items: 多结果集IMultipleResult接口
items: VC++ IPv6的支持
items: Windows下的代码注入
items: VC++ 崩溃处理以及打印调用堆栈
items: WinSock Socket 池
items: WinSock2 API
items: WinSock 完成端口模型

记一次内存泄露调试
type: item
date: 2021-01-10T13:55:00+08:00
description: 首先介绍一下相关背景。最近在测试一个程序时发现，在任务执行完成之后，从任务管理器上来看，内存并没有下降到理论值上。程序在启动完成之后会占用一定的内存，在执行任务的时候，会动态创建一些内存，用于存储任务的执行状态，比如扫描了哪些页面，在扫描过程中一些收发包的记录等等信息。这些中间信息在任务结束之后会被清理掉。任务结束之后，程序只会保存执行过的任务列表，从理论上讲，任务结束之后，程序此时所占内存应该与程序刚启动时占用内存接近，但是实际观察的结果就是任务结束之后，与刚启动之时内存占用差距在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[…

VC+++ 操作word
type: item
date: 2019-04-14T10:50:00+08:00
description: 最近完成了一个使用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(); //创建一个…

使用miniblink 在程序中嵌入浏览器
type: item
date: 2019-03-17T14:01:00+08:00
description: 最近公司产品中自定义浏览器比较老，打开一些支持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页面无法响应键盘消息，我尝试过改成窗口程序，发现改了之后能正…

多结果集IMultipleResult接口
type: item
date: 2019-03-16T10:50:00+08:00
description: 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**)&pIGetData…

VC++ IPv6的支持
type: item
date: 2018-11-24T14:51:00+08:00
description: 最近根据项目需要，要在产品中添加对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 (Co…

Windows下的代码注入
type: item
date: 2018-09-09T15:22:00+08:00
description: 木马和病毒的好坏很大程度上取决于它的隐蔽性，木马和病毒本质上也是在执行程序代码，如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现，在编写这类程序的时候可以考虑将代码注入到其他进程中，借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用，防病毒软件和软件调试中也有很大的用途，最近也简单的研究过这些东西，在这里将它发布出来。想要将代码注入到其他进程并能成功执行需要解决两个问题：第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑，想要破坏原来的执行流程，使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 =…

VC++ 崩溃处理以及打印调用堆栈
type: item
date: 2018-08-28T15:56:00+08:00
description: 我们在程序发布后总会面临崩溃的情况，这个时候一般很难重现或者很难定位到程序崩溃的位置，之前有方法在程序崩溃的时候记录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掉，而且每个位…

WinSock Socket 池
type: item
date: 2018-08-12T16:23:00+08:00
description: 之前在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(…

WinSock2 API
type: item
date: 2018-07-21T16:44:00+08:00
description: 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, …

WinSock 完成端口模型
type: item
date: 2018-07-06T16:50:00+08:00
description: 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}; WSAStar…