最近开始接触PHP，而一般搭建PHP环境使用的都是xampp 这个集成环境，由于之前我的系统中已经安装了mysql服务，所以在启动mysql的时候出现一些列错误，我通过查询各种资料解决了这个问题，现在记录一下，方便日后遇到同样的问题时能够快速解决，也为遇到同样问题的朋友提供一种解决的思路。启动刚开始时我在点击启动mysql的时候发现它一直卡在尝试启动mysql这个位置，xampp提示内容如下:Attempting to start MySQL service...它启动不成功但是也不提示出错，而且查询日志发现没有错误的日志，这个时候我想到应该是我本地之前安装了mysql，导致失败。而且我还将mysql安装成为了服务，后来查询相关资料，有网友说需要将mysql服务的地址改为xampp下mysql所在地址，具体怎么改我就不写了，一般都可以找到，但是我想说的是，这个方式好像在我这边不起作用。那么就干脆一点直接删除服务就好了。sc delete mysql上述命令直接删除mysql这个服务。然后重启xampp，再次启动mysql，它终于报错了。只要报错就好说了，现在来查询日志，发现日志如下:2018-10-13 22:52:19 37d0 InnoDB: Warning: Using innodb_additional_mem_pool_size is DEPRECATED. This option may be removed in future releases, together with the option innodb_use_sys_malloc and with the InnoDB's internal memory allocator. 2018-10-13 22:52:19 14288 [Note] InnoDB: innodb_empty_free_list_algorithm has been changed to legacy because of small buffer pool size. In order to use backoff, increase buffer pool at least up to 20MB. 2018-10-13 22:52:19 14288 [Note] InnoDB: Using mutexes to ref count buffer pool pages 2018-10-13 22:52:19 14288 [Note] InnoDB: The InnoDB memory heap is disabled 2018-10-13 22:52:19 14288 [Note] InnoDB: Mutexes and rw_locks use Windows interlocked functions 2018-10-13 22:52:19 14288 [Note] InnoDB: _mm_lfence() and _mm_sfence() are used for memory barrier 2018-10-13 22:52:19 14288 [Note] InnoDB: Compressed tables use zlib 1.2.3 2018-10-13 22:52:19 14288 [Note] InnoDB: Using generic crc32 instructions 2018-10-13 22:52:19 14288 [Note] InnoDB: Initializing buffer pool, size = 16.0M 2018-10-13 22:52:19 14288 [Note] InnoDB: Completed initialization of buffer pool 2018-10-13 22:52:19 14288 [Note] InnoDB: Highest supported file format is Barracuda. 2018-10-13 22:52:19 14288 [Warning] InnoDB: Resizing redo log from 2*3072 to 2*320 pages, LSN=1600607 2018-10-13 22:52:19 14288 [Warning] InnoDB: Starting to delete and rewrite log files. 2018-10-13 22:52:19 14288 [Note] InnoDB: Setting log file C:\xampp\mysql\data\ib_logfile101 size to 5 MB 2018-10-13 22:52:19 14288 [Note] InnoDB: Setting log file C:\xampp\mysql\data\ib_logfile1 size to 5 MB 2018-10-13 22:52:19 14288 [Note] InnoDB: Renaming log file C:\xampp\mysql\data\ib_logfile101 to C:\xampp\mysql\data\ib_logfile0 2018-10-13 22:52:19 14288 [Warning] InnoDB: New log files created, LSN=1601036 2018-10-13 22:52:19 14288 [Note] InnoDB: 128 rollback segment(s) are active. 2018-10-13 22:52:19 14288 [Note] InnoDB: Waiting for purge to start 2018-10-13 22:52:19 14288 [Note] InnoDB: Percona XtraDB (http://www.percona.com) 5.6.39-83.1 started; log sequence number 1600607 2018-10-13 22:52:20 3508 [Note] InnoDB: Dumping buffer pool(s) not yet started 2018-10-13 22:52:20 14288 [Note] Plugin 'FEEDBACK' is disabled. 2018-10-13 22:52:20 14288 [ERROR] Could not open mysql.plugin table. Some plugins may be not loaded 2018-10-13 22:52:20 14288 [ERROR] Can't open and lock privilege tables: Table 'mysql.servers' doesn't exist 2018-10-13 22:52:20 14288 [Note] Server socket created on IP: '::'. 2018-10-13 22:52:20 14288 [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist 2018-10-13 22:55:18 3024 InnoDB: Warning: Using innodb_additional_mem_pool_size is DEPRECATED. This option may be removed in future releases, together with the option innodb_use_sys_malloc and with the InnoDB's internal memory allocator. 2018-10-13 22:55:18 12324 [Note] InnoDB: innodb_empty_free_list_algorithm has been changed to legacy because of small buffer pool size. In order to use backoff, increase buffer pool at least up to 20MB.找到其中的ERROR项，发现它提示mysql.user这个表不存在，这个表保存的是mysql的账号信息，如果没有这个，它无法知道哪些是合法用户，合法用户又有哪些权限，因此这里就需要创建这个表。通过查询资料发现这是由于未进行mysql数据初始化的缘故，这个错误经常见于通过源码包在编译安装的时候。这个时候需要使用命令 mysql_install_db 来初始化数据库表mysql_install_db --user=mysql -d C:\xampp\mysql\data\-d 后面跟上mysql表数据所在路径执行之后发现程序又报错了，这次提示mysql的版本不对Can't find messagefile "D:\mysql-8.0.11-winx64\share\errmsg.sys". Probably from another version of MariaDB这个时候就很奇怪了，我启动的是xampp中的mysql，为何它给我定位的是之前安装的MySQL所在路径呢？出现这种现象肯定是系统中的相关配置的路径不对，之前已经删掉了mysql服务，那么应该不可能会是服务配置导致的，剩下的应该就是环境变量了，通过一个个的查看环境变量，终于发现了 MYSQL_HOME这个变量给的是 D:\mysql-8.0.11-winx64 这个路径，我们将这个环境变量的值修改为xampp中mysql的路径然后再执行命令初始化mysql表数据，这个时候成功了。完成了这些操作，我这边就可以通过xampp面板启动mysql了。数据库配置刚开始时使用root账户登录是不需要密码的，这样是很危险的操作，容易发生数据泄露，为了安全起见，首先给root账户输入一个复杂的密码mysqladmin -uroot -p password回车之后它会让你输入新的密码，如果是修改密码可以使用下面的命令mysqladmin -uroot -p"test" password其中test为原始密码在回车之后它会让你输入新的密码我们为root设置了一个相对复杂的密码，但是与Linux系统相似，为了安全一般不能随便给出root账户，这个时候就需要一个非root账户并为它设置相关权限，我们可以在进入mysql后，使用grant 命令来创建账户以及分配权限grant all privileges on *.* to masimaro@localhost identified by "masimarotest"; flush privileges;它的语法格式为: grant 权限 on 数据库.表 to 用户名@主机 identified by "密码" 权限，all privileges 表示所有权限，如果不想分配所有权限，可以考虑使用 select,insert,update,delete,create,drop,index,alter,grant,references,reload,shutdown,process,file 权限中的任意一个或者多个。数据库，表：我们可以指定具体的用户对具体的数据库表有何种权限主机:主机可以是localhost，%（任意主机）,或者具体的主机名、ip等等，表示这个账户只能通过对应的主机来登录分配完成之后通过 flush privileges; 语句来保存我们分配的账户和权限为了方便操作，还可以对phpmyadmin进行配置，以便能够使用phpmyadmin来连接并操作mysql数据库。可以在phpmyadmin目录中找到 config.inc.php 文件，找到这么几行$cfg['Servers'][$i]['user'] = ''; //连接数据库的用户 $cfg['Servers'][$i]['password'] = ''; //连接数据库的用户密码 $cfg['Servers'][$i]['host'] = '127.0.0.1'; //数据库所在主机 $cfg['Servers'][$i]['controluser'] = 'root'; //phpmyadmin 所使用的配置账户 $cfg['Servers'][$i]['controlpass'] = ''; //配置账户的密码根据具体情况配置这些信息之后，就可以直接连上PHPmyadmin了，然后根据它的提示来初始化相关数据库和表即可
木马和病毒的好坏很大程度上取决于它的隐蔽性，木马和病毒本质上也是在执行程序代码，如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现，在编写这类程序的时候可以考虑将代码注入到其他进程中，借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用，防病毒软件和软件调试中也有很大的用途，最近也简单的研究过这些东西，在这里将它发布出来。想要将代码注入到其他进程并能成功执行需要解决两个问题：第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑，想要破坏原来的执行流程，使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的上述问题，提出了完成端口的解决方案，完成事件由对应的线程处理，而主线程只需要专注于它自己的工作就好了，这就相当于警报响了，我们知道信来了，直接派一个人去拿信，后面的我就不管了，而拿信的人把信拿回来的时候将信放好。当我们忙完之后去处理这封信。没忙完的话信就一直放在那，甚至让拿信的人处理这封信，这样就能更高效的集中注意力来处理眼前的工作。
title: WinSock 重叠IO模型tags: [WinSock 模型, 网络编程, 重叠IO模型]date: 2018-06-29 20:26:13categories: Windows 网络编程keywords: WinSock 模型, 网络编程, 重叠IO模型之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题，但是网卡这种设备相比于CPU和内存来说仍然是慢速设备，而调用send和recv进行数据收发操作仍然是同步的操作，即使我们能够在恰当的时机调用对应的函数进行收发操作，但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题，这篇博文介绍的重叠IO模型将解决这个等待的问题之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题，但是网卡这种设备相比于CPU和内存来说仍然是慢速设备，而调用send和recv进行数据收发操作仍然是同步的操作，即使我们能够在恰当的时机调用对应的函数进行收发操作，但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题，这篇博文介绍的重叠IO模型将解决这个等待的问题重叠IO简介一般接触重叠IO最早是在读写磁盘时提出的一种异步操作模型，它主要思想是CPU只管发送读写的命令，而不用等待读写完成，CPU发送命令后接着去执行自己后面的命令，至于具体的读写操作由硬件的DMA来控制，当读写完成时会向CPU发送一个终端信号，此时CPU中断当前的工作转而去进行IO完成的处理。这是在磁盘操作中的一种高效工作的方式，为什么在网络中又拿出来说呢？仔细想想，前面的模型解决了接收数据的时机问题，现在摆在面前的就是如何高效的读写数据，与磁盘操作做类比，当接收到WSAAsyncSelect对应的消息或者WSAEvent返回时就是执行读写操作的时机，下面紧接着就是调用对应的读写函数来进行读写数据了，而联想到linux中的一切皆文件的思想，我们是不是可以认为操作网卡也是在操作文件？这也是在WinSock1中，使用WriteFile和ReadFile来进行网络数据读写的原因。既然它本质上也是CPU需要等待慢速的设备，那么为了效率它必定可以支持异步操作，也就可以使用重叠IO。创建重叠IO的socket要想使用重叠IO，就不能在像之前那样使用socket函数来创建SOCKET, 这函数最多只能创建一个普通SOCKET然后设置它为非阻塞(请注意非阻塞与异步的区别)。要创建异步的SOCKET需要使用WinSock2.0函数 WSASocketSOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );该函数的前3个参数与socket的参数含义相同,第4个参数是一个协议的具体信息，配合WSAEnumProtocols 使用可以将枚举出来的网络协议信息传入，这样不通过前三个参数就可以创建一个针对具体协议的SOCKET。第5个参数目前不受支持简单的传入0即可。第6个参数是一个标志，如果要创建重叠IO的SOCKET，需要将这个参数设置为WSA_FLAG_OVERLAPPED。否则普通的SOCKET直接传入0即可使用重叠IO除了要将SOCKET设置为支持重叠IO外，还需要使用对应的支持重叠IO的函数，之前了解的巴克利套接字函数最多只能算是支持非阻塞而不支持异步。在WinSock1.0 中可以使用ReadFile和WriteFile来支持重叠IO，但是WinSock2.0 中重新设计的一套函数来支持重叠IOWSASend (send的等价函数)WSASendTo (sendto的等价函数)WSARecv (recv的等价函数)WSARecvFrom (recvfrom的等价函数)WSAIoctl (ioctlsocket的等价函数)WSARecvMsg (recv OOB版的等价函数)AcceptEx (accept 等价函数)ConnectEx (connect 等价函数)TransmitFile (专门用于高效发送文件的扩展API)TransmitPackets (专门用于高效发送大规模数据包的扩展API)DisconnectEx (扩展的断开连接的Winsock API)WSANSPIoctl (用于操作名字空间的重叠I/O版扩展控制API)那么如果使用上述函数但是传入一个非阻塞的SOCKET会怎么样呢，这些函数只看是否传入OVERLAPPED结构而不管SOCKET是否是阻塞的，一律按重叠IO的方式来运行。这也就是说,要使用重叠I/O方式来操作SOCKET,那么不一定非要一开初就创建一个重叠I/O方式的SOCKET对象(但是针对AcceptEx 来说如果传入的是普通的SOCKET，它会以阻塞的方式执行。当时测试时我传入的是使用WSASocket创建的SOCKET，我将函数的最后一个标志设置为0，发现AcceptEx只有当客户端连接时才会返回)重叠IO的通知模型与文件的重叠IO类似，重叠IO的第一种模型就是事件通知模型.利用该模型首先需要把一个event对象绑定到OVERLAPPED(WinSokc中一般是WSAOVERLAPPED)上，然后利用这个OVERLAPPED结构来进行IO操作.如:WSASend/WSARecv等判断对应IO操作的返回值，如果使用重叠IO模式，IO操作函数不会返回成功，而是会返回失败，使用WSAGetLastError得到的错误码为WSA_IO_PENDING,此时认为函数进行一种待决状态，也就是CPU将命令发送出去了，而任务没有最终完成然后CPU可以去做接下来的工作，而在需要操作结果的地方调用对应的等待函数来等待对应的事件对象。如果事件对象为有信号表示操作完成接着可以设置事件对象为无信号，然后继续投递IO操作.要等待这些事件句柄,可以调用WSAWaitForMultipleEvents函数,该函数原型如下:DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in BOOL fAlertable );第一个参数是事件对象的数目；第二个参数是事件对象的数组首地址；第三个参数是一个bool类型表示是否等待数组中所有的对象都变为有信号；第四个参数表示超时值；第五个参数是表示在等待的时候是否进入可警告状态在函数返回后我们只知道IO操作完成了，但是完成的结果是成功还是失败是不知道的，此时可以使用WSAGetOverlappedResult来确定IO操作执行的结果，该函数原型如下:BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags );第一个参数是对应的socket；第二个参数是对应的OVERLAPPED结构；第三个参数是一个输出参数，表示完成IO操作的字节数，通常出错的时候返回0；第四个参数指明调用者是否等待一个重叠I/O操作完成,通常在成功等待到事件句柄后,这个参数在这个模型中没有意义了；第五个参数是一个输出参数负责接收完成结果的标志。下面是一个事件通知模型的例子typedef struct _tag_CLIENTCONTENT { OVERLAPPED Overlapped; SOCKET sClient; WSABUF DataBuf; char szBuf[WSA_BUFFER_LENGHT]; WSAEVENT hEvent; }CLIENTCONTENT, *LPCLIENTCONTENT; int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); CLIENTCONTENT ClientContent[WSA_MAXIMUM_WAIT_EVENTS] = {0}; WSAEVENT Event[WSA_MAXIMUM_WAIT_EVENTS] = {0}; int nTotal = 0; SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerAddr = {0}; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(SERVER_PORT); ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerAddr, sizeof(SOCKADDR)); listen(skServer, 5); printf("开始监听...........\n"); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].sClient = skServer; //针对监听套接字做特殊的处理 WSAEventSelect(skServer, Event[0], FD_ACCEPT | FD_CLOSE); nTotal++; while (TRUE) { DWORD dwTransfer = 0; DWORD dwFlags = 0; DWORD dwNumberOfBytesRecv = 0; int nIndex = WSAWaitForMultipleEvents(nTotal, Event, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(Event[nIndex - WSA_WAIT_EVENT_0]); //监听socket返回 if (nIndex - WSA_WAIT_EVENT_0 == 0) { SOCKADDR_IN ClientAddr = {AF_INET}; int nClientAddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&ClientAddr, &nClientAddrSize, NULL, NULL); if (SOCKET_ERROR == skClient) { printf("接受客户端连接请求失败，错误码为:%08x\n", WSAGetLastError()); continue; } printf("有客户端连接进来[%s:%u]\n", inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port)); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].DataBuf.buf = ClientContent[nTotal].szBuf; ClientContent[nTotal].sClient = skClient; //获取客户端发送数据,这是为了触发后面的等待 WSARecv(ClientContent[nTotal].sClient, &ClientContent[nTotal].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nTotal].Overlapped, NULL); nTotal++; continue; }else { //等待发送完成 WSAGetOverlappedResult(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, &dwTransfer, TRUE, &dwFlags); if (dwTransfer == 0) { printf("接受数据失败:%08x\n", WSAGetLastError()); closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } } if (strcmp("exit", ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf) == 0) { closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } continue; } send(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf, dwTransfer, 0); WSARecv(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, NULL); } } WSACleanup(); return 0; }上述代码中定义了一个结构，方便我们根据事件对象获取一些重要信息。在main函数中首先完成了WinSock环境的初始化然后创建监听套接字，绑定，监听。然后定义一个事件对象让他与对应的WSAOVERLAPPED绑定,然后WSAEventSelect来投递监听SOCKET以便获取到客户端的连接请求（这里没有使用AcceptEx，因为它需要特殊的加载方式）接着在循环中首先调用WSAWaitForMultipleEvents等待所有信号，当函数返回时判断当前是否为监听套接字，如果是那么调用WSAAccept函数接收连接,并准备对应的事件和WSAOVERLAPPED结构，接着调用WSARecv接收客户端传入数据如果不是监听套接字则表明客户端发送数据过来，此时调用WSAGetOverlappedResult获取重叠IO执行的结果，如果成功则判断是否为exit，如果是exit关闭当前与客户端的链接，否则调用send函数原样返回数据接着调用WSARecv再次等待客户端传送数据。完成过程模型对于重叠I/O模型来说,前面的事件通知模型在资源的消耗上有时是惊人的。这主要是因为对于每个重叠I/O操作(WSASend/WSARecv等)来说,都必须额外创建一个Event对象。对于一个I/O密集型SOCKET应用来说,这种消耗会造成资源的严重浪费。由于Event对象是一个内核对象，它在应用层表现为一个4字节的句柄值，但是在内核中它对应的是一个具体的结构，而且所有的进程共享同一块内核的内存，因此某几个进程创建大量的内核对象的话，会影响整个系统的性能。为此重叠I/O又提供了一种称之为完成过程方式的模型。该模型不需要像前面那样提供对应的事件句柄。它需要为每个I/O操作提供一个完成之后回调处理的函数。完成历程的本质是一个历程它仍然是使用当前线程的环境。它主要向系统注册一些完成函数，当对应的IO操作完成时，系统会将函数放入到线程的APC队列，当线程陷入可警告状态时，它利用线程的环境来依次执行队列中的APC函数、要使用重叠I/O完成过程模型,那么也需要为每个I/O操作提供WSAOVERLAPPED结构体,只是此时不需要Event对象了。取而代之的是提供一个完成过程的函数完成历程的原型如下:void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);要使对应的完成函数能够执行需要在恰当的时机让对应线程进入可警告状态，一般的方式是调用SleepEx函数，还有就是调用Wait家族的相关Ex函数，但是如果使用Wait函数就需要使用一个内核对象进行等待，如果使用Event对象这样就与之前的事件通知模式有相同的资源消耗大的问问题了。此时我们可以考虑使用线程的句柄来进行等待，但是等待线程句柄时必须设置一个超时值而不能直接使用INFINIT了，因为等待线程就是要等到线程结束，而如果使用INFINIT，这样Wait函数永远不会返回，线程永远不会结束，此时就造成了死锁。下面是一个使用完成过程的模型typedef struct _tag_OVERLAPPED_COMPILE { WSAOVERLAPPED overlapped; LONG lNetworks; SOCKET sClient; WSABUF pszBuf; DWORD dwTransfer; DWORD dwFlags; DWORD dwNumberOfBytesRecv; DWORD dwNumberOfBytesSend; }OVERLAPPED_COMPILE, *LPOVERLAPPED_COMPILE; void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags); int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerClient = {0}; ServerClient.sin_family = AF_INET; ServerClient.sin_port = htons(SERVER_PORT); ServerClient.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerClient, sizeof(SOCKADDR)); listen(skServer, 0); while (TRUE) { SOCKADDR_IN AddrClient = {0}; int AddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&AddrClient, &AddrSize, NULL, NULL); printf("有客户端[%s:%u]连接进来....\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); LPOVERLAPPED_COMPILE lpOc = new OVERLAPPED_COMPILE; ZeroMemory(lpOc, sizeof(OVERLAPPED_COMPILE)); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; lpOc->pszBuf.buf = new char[1024]; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->pszBuf.len = 1024; lpOc->sClient = skClient; lpOc->dwNumberOfBytesRecv = 0; WSARecv(skClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); SleepEx(2000, TRUE); } WSACleanup(); return 0; } void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) { LPOVERLAPPED_COMPILE lpOc = (LPOVERLAPPED_COMPILE)lpOverlapped; if (0 != dwError || 0 == cbTransferred) { printf("与客户端通信发生错误，错误码为:%08x\n", WSAGetLastError()); closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } if (lpOc->lNetworks == FD_READ) { if (0 == strcmp(lpOc->pszBuf.buf, "exit")) { closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } send(lpOc->sClient, lpOc->pszBuf.buf, cbTransferred, 0); lpOc->dwNumberOfBytesRecv = 0; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; WSARecv(lpOc->sClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); } }主函数的写法与之前的例子中的写法类似。也是先初始化环境，绑定，监听等等。在循环中接收连接，当有新客户端连接进来时创建对应的客户端结构，然后调用WSARecv函数接收数据，接下来就是使用SleepEx进入可警告状态，以便让完成历程有机会执行。在完成历程中就不需要像之前那样调用WSAGetOverlappedResult了，因为调用完成历程就一定意味着重叠IO操作已经完成了。在完成历程中根据第一个参数来判断IO操作执行是否成功。如果失败则会直接断开与客户端的连接然后清理对应的结构。如果成功则直接获取获取IO操作得到的数据，如果是exit则需要关闭连接，否则原样返回并准备下一次接收数据
在前面我们说了WSAAsyncSelect 模型，它相比于select模型来说提供了这样一种机制：当发生对应的IO通知时会立即通知操作系统，并调用对应的处理函数，它解决了调用send和 recv的时机问题，但是它有一个明显的缺点，就是它必须依赖窗口。对此WinSock 提供了另一种模型 WSAEventSelect模型简介该模型主要特色在于它使用事件句柄来完成SOCKET事件的通知。与WSAAsyncSelect 模型类似，它也允许使用事件对象来完成多个socket的完成通知。该模型首先在每个socket句柄上调用WSACreateEvent来创建一个WSAEvent对象句柄（早期的WSAEvent与传统的Event句柄有一定的区别，但是从WinSock2.0 以后二者是同一个东西）。接着调用WSAEventSelect将SOCKET句柄和WSAEvent对象绑定，最终通过WSAWaitForMultiEvents来等待WSAEvent变为有信号，然后再来处理对应的socketWSAEvent有两种工作模式和工作状态工作状态有有信号和无信号两种工作模式有手工重置和人工重置，手工重置指的是每当WSAWaitForMultiEvents或者WSAWaitForSingleEvents 返回之后，WSAEvent不会自动变为无信号，需要手工调用WSAResetEvent来将WSAEvent对象设置为无信号，而自动重置表示每次等待函数返回后会自动重置为无信号；调用WSACreateEvent创建的WSAEvent对象是需要手工重置的，如果想创建自动重置的WSAEvent对象可以调用CreateEvent函数来创建（由于WinSock2.0 之后二者没有任何区别，所以只需要调用CreateEvent并将返回值强转为WSAEvent即可）WSAEventSelect函数的原型如下:int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);其中s表示对应的SOCKET，hEventObject表示对应的WSAEvent对象，lNetworkEvents 表示我们需要处理哪些事件，它有一些对应的宏定义网络事件对应的含义FD_READ当前可以进行数据接收操作，此时可以调用像 recv, recvfrom, WSARecv, 或者 WSARecvFrom 这样的函数FD_WRITE此时可以发送数据，可以调用 send, sendto, WSASend, or WSASendToFD_ACCEPT可以调用accept (Windows Sockets) 或者 WSAAccept 除非返回的错误代码是WSATRY_AGAIN.FD_CONNECT表示当前可以连接远程服务器FD_CLOSE当前收到关闭的消息当WSAWaitForMultipleEvents返回时同时会返回一个序号，用于标识是数组中的哪个WSAEvent有信号，我们使用 index - WSA_WAIT_EVENT_0 来获取对应WSAEvent在数组中的下标，然后根据这个事件对象找到对应的SOCKET即可获得了对应的SOCKET以后，还需要获取到当前是哪个事件发生导致它变为有信号，我们可以调用WSAEnumNetworkEvents函数来获取对应发生的网络事件int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );s就是要获取其具体事件通知的SOCKET句柄hEventObject就是对应的WSAEvent句柄,可以不传入,因为SOCKET句柄已经说明了要获取那个句柄上的通知,当然如果传入了,那么这个函数会对这个WSAEvent做一次重置,置为无信号的状态,相当于WSAResetEvent调用。此时我们就不需要调用WSAResetEvent函数了最后一个参数是一个结构，结构的定义如下:typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;第一个数据是当前产生的网络事件。iErrorCode数组是对应每个网络事件可能发生的错误代码,对于每个事件错误代码其具体数组下标是预定义的一组FD_开头的串再加上一个_BIT结尾的宏,比如FD_READ事件对应的错误码下标是FD_READ_BIT下面的代码演示了处理接收(读取)数据的事件错误的例子代码if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]); } }到目前为止，我们可以总结一下使用WSAEventSelect模型的步骤调用WSACreateEvent为每一个SOCKET创建一个等待对象，并与对应的SOCKET形成映射关系调用WSAEventSelect函数将SOCKET于WSAEvent对象进行绑定调用WSAWaitForMultipleEvents 函数对所有SOCKET句柄进行等待当WSAWaitForMultipleEvents 函数返回时利用返回的索引找到对应的WSAEvent对象和SOCKET对象调用WSAEnumNetworkEvents来获取对应的网络事件，根据网络事件来进行对应的收发操作重复3~5的步骤示例下面是一个简单的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); SOCKADDR_IN AddrServer = {AF_INET}; AddrServer.sin_port = htons(SERVER_PORT); AddrServer.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&AddrServer, sizeof(SOCKADDR)); listen(skServer, 5); printf("服务端正在监听...........\n"); CWSAEvent WSAEvent; WSAEvent.InsertClient(skServer, FD_ACCEPT | FD_CLOSE); WSAEvent.EventLoop(); WSACleanup(); return 0; }在代码中定义了一个类CWSAEvent，该类封装了关于该模型的相关操作和对应事件对象和SOCKET对象的操作，在主函数中首先创建监听的SOCKET，然后绑定、监听,并提交监听SOCKET到类中，以便对它进行管理,函数InsertClient的定义如下:void CWSAEvent::InsertClient(SOCKET skClient, long lNetworkEvents) { m_socketArray[m_nTotalItem] = skClient; m_EventArray[m_nTotalItem] = WSACreateEvent(); WSAEventSelect(skClient, m_EventArray[m_nTotalItem++], lNetworkEvents); }这个函数中主要向事件数组和SOCKET数组的对应位置添加了相应的成员，然后调用WSAEventSelect。而类的EventLoop函数定义了一个循环来重复前面的3~5步,函数的部分代码如下:int CWSAEvent::WaitForAllClient() { DWORD dwRet = WSAWaitForMultipleEvents(m_nTotalItem, m_EventArray, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(m_EventArray[dwRet - WSA_WAIT_EVENT_0]); return dwRet - WSA_WAIT_EVENT_0; } int CWSAEvent::EventLoop() { WSANETWORKEVENTS wne = {0}; while (TRUE) { int nRet = WaitForAllClient(); WSAEnumNetworkEvents(m_socketArray[nRet], m_EventArray[nRet], &wne); if (wne.lNetworkEvents & FD_ACCEPT) { if (0 != wne.iErrorCode[FD_ACCEPT_BIT]) { OnAcceptError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_ACCEPT_BIT]); }else { OnAcccept(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_CLOSE) { if (0 != wne.iErrorCode[FD_CLOSE_BIT]) { OnCloseError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_CLOSE_BIT]); }else { OnClose(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_READ) { if (0 != wne.iErrorCode[FD_READ_BIT]) { OnReadError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_READ_BIT]); }else { OnRead(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_WRITE) { if (0 != wne.iErrorCode[FD_WRITE_BIT]) { OnWriteError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_WRITE_BIT]); }else { OnWrite(nRet, m_socketArray[nRet]); } } } }函数首先进行了等待，当等待函数返回时，获取对应的下标，以此来获取到socket和事件对象，然后调用WSAEnumNetworkEvents来获取对应的网络事件，最后根据事件调用不同的处理函数来处理在上面的代码中，这个循环有一个潜在的问题，我们来设想这么一个场景，当有多个客户端同时连接服务器，在第一次等待返回时，我们主要精力在进行该IO事件的处理，也就是响应这个客户端A的请求，而此时客户端A又发送了一个请求，而另外几个客户端B随后也发送了一个请求，在第一次处理完成后，等待得到的将又是客户端A，而后续客户端B的请求又被排到了后面，如果这个客户端A一直不停的发送请求，可能造成的问题是服务器一直响应A的请求，而对于B来说，它的请求迟迟得不到响应。为了避免这个问题，我们可以在函数WSAWaitForMultipleEvents 返回后，针对数组中的每个SOCKET循环调用WSAWaitForMultipleEvents将等待的数量设置为1，并将超时值设置为0，这个时候这个函数的作用就相当于查看数组中的每个SOCKET，看看是不是有待决的，当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求最后，整个示例代码
select 模型虽然可以管理多个socket，但是它涉及到一个时机的问题，select模型会针对所管理的数组中的每一个socket循环检测它管理是否在对应的数组中，从时间复杂度上来说它是O(n^2)的,而且还有可能发生数组中没有socket处于待决状态而导致本轮循环做无用功的情况，针对这些问题，winsock中有了新的模型——WSAAsyncSelect 消息模型消息模型的核心是基于Windows窗口消息获得网络事件的通知，Windows窗口是用来与用户交互的，而它并不知道用户什么时候会操作窗口，所以Windows窗口本身就是基于消息的异步通知，网络事件本身也是一个通知消息，将二者结合起来可以很好的使socket通知像消息那样当触发通知时调用窗口过程。这样就解决了select中的时机问题和里面两层循环的问题WSAAsyncSelect函数原型如下:int WSAAsyncSelect( __in SOCKET s, __in HWND hWnd, __in unsigned int wMsg, __in long lEvent );第一个参数是绑定的socket，第二个参数是消息所对应的窗口句柄，第三个参数是对应的消息，这个消息需要自己定义，第4个参数是我们所关心的事件，当在s这个socket发生lEvent这个事件发生时会向hWnd对应的窗口发送wMsg消息。在消息附带的两个参数wParam和lParam中，lParam的高位16位表示当前的错误码，低16位表示当前socket上发生的事件。其中事件的取值如下:FD_WRITE : 当socket上可写时触发该事件，FD_WRITE的触发与调用send没有必然的联系，FD_WRITE只是表示socket已经为发送准备好了必要的条件，其实调用时可以不必理会这个事件，只需要在想发送数据的场合调用send，一般来说FD_WRITE只在这些条件下触发：a) 调用connect函数成功连接到服务器 b) 调用accept接受连接成功后（该条件是绑定在accept返回的那个与客户端通讯的socket上) c）调用send，sendto 失败并返回WSAWOULDBLOCK(由于是异步操作，可能同时客户端也在发数据, 此时可能导致send失败)为了方便我们处理这些参数，WinSock 提供了两个宏来解析它的高16位和低16位，分别是WSAGETSELECTERROR和WSAGETSELECTEVENT而lParam则保存了当前触发事件的socket句柄如果对一个句柄调用了WSAAsyncSelect 并成功后，对应的socket会自动编程非阻塞模式。它就不像前面的select模型那样需要显示调用ioctrlsocket将socekt设置为非阻塞。另外不需要每个socket都定义一个消息ID，通常一个ID已经足够处理所有的socket事件。下面是一个具体的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKADDR_IN SrvAddr = {AF_INET}; SrvAddr.sin_addr.s_addr = htonl(INADDR_ANY); SrvAddr.sin_port = htons(SERVER_PORT); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == skServer) { printf("初始化socket失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != bind(skServer, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR))) { printf("绑定失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != listen(skServer, 5)) { printf("监听失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } RegisterWindow(); CreateAndShowWnd(); g_uSockMsgID = RegisterWindowMessage(SOCKNOTIFY_MESSAGE); WSAAsyncSelect(skServer, g_hMainWnd, g_uSockMsgID, FD_ACCEPT | FD_CLOSE); MessageLoop(); __CLEAR_UP: if (INVALID_SOCKET != skServer) { closesocket(skServer); } WSACleanup(); return 0; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRes = 0; switch (uMsg) { case WM_CLOSE: { CloseWindow(hwnd); DestroyWindow(hwnd); } break; case WM_PAINT: { PAINTSTRUCT ps = {0}; BeginPaint(hwnd, &ps); EndPaint(hwnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; default: if (uMsg == g_uSockMsgID) { lRes = ParseNotifyMessage(wParam, lParam); } lRes = DefWindowProc(hwnd, uMsg, wParam, lParam); } return lRes; } LRESULT ParseNotifyMessage(WPARAM wParam, LPARAM lParam) { WORD wNotify = WSAGETSELECTEVENT(lParam); WORD wError = WSAGETSELECTERROR(lParam); if (wNotify == FD_ACCEPT) { return OnAcceptMsg((SOCKET)wParam, lParam); }else if (wNotify == FD_READ) { return OnReadMsg((SOCKET)wParam, lParam); } return 1; } LRESULT OnAcceptMsg(SOCKET s, LPARAM lParam) { SOCKADDR_IN AddrClient = {0}; int nAddrSize = sizeof(SOCKADDR); SOCKET sClient = accept(s, (SOCKADDR*)&AddrClient, &nAddrSize); printf("有客户端连接进来[%s:%u]\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); return WSAAsyncSelect(sClient, g_hMainWnd, g_uSockMsgID, FD_WRITE | FD_READ | FD_CLOSE); } LRESULT OnReadMsg(SOCKET s, LPARAM lParam) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024); ZeroMemory(pszBuf, 1024); int nTotalSize = 0; int i = 1; while (TRUE) { i++; int nReadSize = recv(s, pszBuf + nTotalSize, 1024, 0); if (nReadSize < 1024) { nTotalSize += nReadSize; break; } nTotalSize += nReadSize; HeapReAlloc(GetProcessHeap(), 0, pszBuf, 1024 * i); } if (strcmp(pszBuf, "exit") == 0) { shutdown(s, SD_BOTH); closesocket(s); } send(s, pszBuf, nTotalSize, 0); HeapFree(GetProcessHeap(), 0, pszBuf); return 0; } 在上面的代码中我们在main函数中创建了窗口程序，而常规的都是在WinMain中创建，其实从本质上讲控制台程序和窗口程序都是一个进程，至于以main作为入口还是以WinMain作为入口只是习惯上这样，但是并没有硬性规定。 在创建窗口之后我们将监听socket也绑定到窗口消息中，然后在对应的消息中判断FD_ACCEPT事件，如果是则调用accept进行连接。并将对生成的socket进行绑定。 在接下来的socket消息中主要处理FD_READ事件，当发生READ事件时调用read接收数据，然后调用send将数据原封不动的发送出去。 从上面的代码上看，该模型相对于select来说省去了查看socket是否在对应数组中的操作，减少了循环。而且可以很好的把握什么调用时机问题。 主要的缺点是它需要一个窗口，这样在服务程序中基本就排除掉了这个模型，它基本上只会出现在客户端程序中。 另外如果在一个窗口中需要管理成千上万个句柄时，它的性能会急剧下降，因此它的伸缩性较差。但是在客户端中基本不存在这个问题，所以如果要在客户端中想要减少编程难度，它是一个不二的选择
上次将OLEDB的所有内容基本上都说完了，从之前的示例上来看OLEDB中有许多变量的定义，什么结果集对象、session对象、命令对象，还有各种缓冲等等，总体上来说直接使用OLEDB写程序很麻烦，用很大的代码量带来的仅仅只是简单的功能。还要考虑各种缓冲的释放，各种对象的关闭，程序员的大量精力都浪费在无用的事情上，针对这些情况微软在OLEDB上提供了两种封装方式，一种是将其封装在ATL模板库中，一种是使用ActiveX控件来进行封装称之为ADO，这次主要写的是这两种方式ATL 模板中的OLEDB由于ATL模板是开源的，这种方式封装简洁，调试简易（毕竟源代码都给你了），各个模块相对独立，但是它的缺点很明显就是使用门槛相对较高，只有对C++中的模板十分熟悉的开发人员才能使用的得心应手。ATL中的OLEDB主要有两大模块，提供者模块和消费者模块，顾名思义，提供者模块是数据库的开发人员使用的，它主要使用这个模块实现OLEDB中的接口，对外提供相应的数据库服务；消费者模块就是使用OLEDB在程序中操作数据库。这里主要说的是消费者模块ATL主要封装的类ATL针对OLEDB封装的主要有这么几个重要的类：数据库对象CDataConnection 数据源连接类主要实现的是数据库的连接相关的功能，根据这个可以猜测出来它实际上封装的是OLEDB中的数据源对象和会话对象CDataSource：数据源对象CEnumerator: 架构结果集对象，主要用来查询数据库的相关信息，比如数据库中的表结构等信息CSession: 会话对象访问器对象：CAccessor: 常规的访问器对象CAccessorBase: 访问器对象的基类CDynamicAccessor：动态绑定的访问器CDynamicParamterAccessor:参数绑定的访问器,从之前博文的内容来看它应该是进行参数化查询等操作时使用的对象CDynamicStringAccessor:这个一般是要将查询结果显示为字符串时使用，它负责将数据库中的数据转化为字符串ALT中针对OLEDB的封装在头文件atldbcli.h中，在项目中只要包含它就行了模板的使用静态绑定针对静态绑定，VS提供了很好的向导程序帮助我们生成对应的类，方便了开发，使用的基本步骤如下:在项目上右键，选择添加类在类选择框中点击ATL并选择其中的ATL OLEDB使用者选择对应的数据源、数据库表和需要对数据库进行的操作注意如果要对数据库表进行增删改查等操作，一定要选这里的表选项点击数据源配置数据源连接的相关属性，最后点击完成。最终会在项目中生成对应的头文件这是最终生成的完整代码class Caa26Accessor { public: //value LONG m_aac031; TCHAR m_aaa146[51]; LONG m_aaa147; LONG m_aaa148; //status DBSTATUS m_dwaac031Status; DBSTATUS m_dwaaa146Status; DBSTATUS m_dwaaa147Status; DBSTATUS m_dwaaa148Status; //lenth DBLENGTH m_dwaac031Length; DBLENGTH m_dwaaa146Length; DBLENGTH m_dwaaa147Length; DBLENGTH m_dwaaa148Length; void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_IRowsetChange, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); } HRESULT OpenDataSource() { CDataSource _db; HRESULT hr; hr = _db.OpenFromInitializationString(L"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa; Password=XXXXXX;Initial Catalog=study;Data Source=XXXX;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LIU-PC;Use Encryption for Data=False;Tag with column collation when possible=False"); if (FAILED(hr)) { #ifdef _DEBUG AtlTraceErrorRecords(hr); #endif return hr; } return m_session.Open(_db); } void CloseDataSource() { m_session.Close(); } operator const CSession&() { return m_session; } CSession m_session; DEFINE_COMMAND_EX(Caa26Accessor, L" \ SELECT \ aac031, \ aaa146, \ aaa147, \ aaa148 \ FROM dbo.aa26") BEGIN_COLUMN_MAP(Caa26Accessor) COLUMN_ENTRY_LENGTH_STATUS(1, m_aac031, m_dwaac031Length, m_dwaac031Status) COLUMN_ENTRY_LENGTH_STATUS(2, m_aaa146, m_dwaaa146Length, m_dwaaa146Status) COLUMN_ENTRY_LENGTH_STATUS(3, m_aaa147, m_dwaaa147Length, m_dwaaa147Status) COLUMN_ENTRY_LENGTH_STATUS(4, m_aaa148, m_dwaaa148Length, m_dwaaa148Status) END_COLUMN_MAP() }; class Caa26 : public CCommand<CAccessor<Caa26Accessor> > { public: HRESULT OpenAll() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; __if_exists(GetRowsetProperties) { CDBPropSet propset(DBPROPSET_ROWSET); __if_exists(HasBookmark) { if( HasBookmark() ) propset.AddProperty(DBPROP_IRowsetLocate, true); } GetRowsetProperties(&propset); return OpenRowset(&propset); } __if_not_exists(GetRowsetProperties) { __if_exists(HasBookmark) { if( HasBookmark() ) { CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetLocate, true); return OpenRowset(&propset); } } } return OpenRowset(); } HRESULT OpenRowset(DBPROPSET *pPropSet = NULL) { HRESULT hr = Open(m_session, NULL, pPropSet); #ifdef _DEBUG if(FAILED(hr)) AtlTraceErrorRecords(hr); #endif return hr; } void CloseAll() { Close(); ReleaseCommand(); CloseDataSource(); } };从名字上来看Caa26Accessor主要是作为一个访问器，其实它的功能也是与访问器相关的，比如创建访问器和数据绑定都在最后这个映射中。而后面的Caa26类主要是用来执行sql语句并根据上面的访问器类来解析数据，其实我们使用上主要使用后面这个类，这些代码都很简单，有之前的OLEDB基础很容易就能理解它们，这里就不再在这块进行展开了int _tmain(int argc, TCHAR *argv) { CoInitialize(NULL); Caa26 aa26; HRESULT hRes = aa26.OpenDataSource(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.OpenRowset(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.MoveNext(); COM_USEPRINTF(); do { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), aa26.m_aac031, aa26.m_aaa146, aa26.m_aaa147, aa26.m_aaa148); hRes = aa26.MoveNext(); } while (S_OK == hRes); aa26.CloseAll(); CoUninitialize(); return 0; }动态绑定动态绑定主要是使用模板对相关的类进行拼装，所以这里需要关于模板的相关知识，只有掌握了这些才能正确的拼接出合适的类。一般需要拼接的是这样几个类结果集类，在结果集类的模板中填入对应的访问器类，表示该结果集将使用对应的访问器进行解析。访问器类可以系统预定义的，也向静态绑定那样自定义。Command类，在命令对象类的模板位置填入与命令相关的类，也就是执行命令生成的结果集、以及解析结果集所用的访问器，之后就主要使用Command类来进行数据库的相关操作了下面是一个使用的示例typedef CCommand<CDynamicAccessor, CRowset, CMultipleResults> CComRowset; typedef CTable<CDynamicAccessor, CRowset> CComTable; //将所有绑定的数据类型转化为字符串 typedef CTable<CDynamicStringAccessor, CRowset> CComTableString; int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); COM_USEPRINTF(); //连接数据库，创建session对象 CDataSource db; db.Open(); CSession session; session.Open(db); //打开数据库表 CComTableString table; table.Open(session, OLESTR("T_DecimalDemo")); HRESULT hRes = table.MoveFirst(); if (FAILED(hRes)) { COM_PRINTF(_T("表中没有数据,退出程序\n")); goto __CLEAN_UP; } do { //这里传入的参数是列的序号，注意一下，由于有第0列的行需要存在，所以真正的数据是从第一列开始的 COM_PRINTF(_T("|%-10s|%-20s|%-20s|%-20s|\n"), table.GetString(1), table.GetString(2), table.GetString(3), table.GetString(4)); hRes = table.MoveNext(); } while (S_OK == hRes); __CLEAN_UP: CoUninitialize(); return 0; }在上面的代码中我们定义了两个模板类，Ctable和CCommand类，没有发现需要的访问器类，查看CTable类可以发现它是继承于CAccessorRowset，而CAccessorRowset继承于TAccessor和 TRowset,也就是说它提供了访问器的相关功能而且它还可以使用OpenRowset方法不执行SQL直接打开数据表，因此在这里我们选择使用它在CTable的模板中填入CDynamicStringAccessor表示将会把得到的结果集中的数据转化为字符串。在使用上先使用CDataSource类的Open方法打开数据库连接，然后调用CTable的Open打开数据表，接着调用CTable的MoveFirst的方法将行句柄移动到首行。接着在循环中调用table的GetString方法得到各个字段的字符串值，并调用MoveNext方法移动到下一行其实在代码中并没有使用CCommand类，这是由于这里只是简单的使用直接打开数据表的方式，而并没有执行SQL语句，因此不需要它，在这里定义它只是简单的展示一下ADOATL针对OLEDB封装的确是方便了不少，但是对于像我这种将C++简单的作为带对象的C来看的人来说，它使用模板实在是太不友好了，说实话现在我现在对模板的认识实在太少，在代码中我也尽量避免使用模板。所以在我看来使用ATL还不如自己根据项目封装一套。好在微软实在太为开发者着想了，又提供了ADO这种针对ActiveX的封装方式。要使用ADO组件需要先导入，导入的语句如下:#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile")这个路径一般是不会变化的，而EOF在C++中一般是用在文件中的，所以这里将它rename一下ADO中的主要对象和接口有:Connect ：数据库的连接对象，类似于OLEDB中的数据源对象和session对象Command：命令对象，用来执行sql语句，类似于OLEDB中的Command对象Recordset: 记录集对象，执行SQL语句返回的结果，类似于OLEDB中的结果集对象Record: 数据记录对象，一般都是从Recordset中取得，就好像OLEDB中从结果集对象通过访问器获取到具体的数据一样Field：记录中的一个字段，可以简单的看做就是一个表字段的值，一般一个记录集中有多条记录，而一条记录中有个Field对象Parameter：参数对象，一般用于参数化查询或者调用存储过程Property：属性，与之前OLEDB中的属性对应-在ADO中大量使用智能指针，所谓的智能指针是它的生命周期结束后会自动析构它所指向的对象，同时也封装了一些常见指针操作，虽然它是这个对象但是它的使用上与普通的指针基本上相同。ADO中的智能指针对象一般是在类名后加上Ptr。比如Connect对象的智能指针对象是_ConnectPtr智能指针有利也有弊，有利的地方在于它能够自动管理内存，不需要程序员进行额外的释放操作，而且它在使用上就像普通的指针，相比于使用类的普通指针更为方便，不利的地方在于为了方便它的使用一般都经过了大量的重载，因此很多地方表面上看是一个普通的寻址操作，而实际上却是一个函数调用，这样就降低了性能。所以在特别强调性能的场合要避免使用智能指针。在使用上，一般经过这样几个步骤：定义数据库连接的Connect对象调用Connect对象的Open方法连接数据库，这里使用的连接字串的方式创建Command对象并调用对象Execute方法执行SQL，并获取对应的记录集。这里执行SQL语句也可以使用Recordset对象的Open方法。循环调用Recordse对象的MoveNext不断取出对应行的行记录下面是一个使用的简单例子#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile") int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); _ConnectionPtr conn; _RecordsetPtr rowset; _bstr_t bstrConn = _T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;"); conn.CreateInstance(_T("ADODB.Connection")); conn->Open(bstrConn, _T("sa"), _T("123456"), adModeUnknown); if (conn->State) { COM_PRINTF(_T("连接到数据源成功\n")); }else { COM_PRINTF(_T("连接到数据源失败\n")); return 0; } rowset.CreateInstance(__uuidof(Recordset)); rowset->Open(_T("select * from aa26;"), conn.GetInterfacePtr(), adOpenStatic, adLockOptimistic, adCmdText); while (!rowset->EndOfFile) { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), rowset->Fields->GetItem(_T("aac031"))->Value.intVal, rowset->Fields->GetItem(_T("aaa146"))->Value.bstrVal, rowset->Fields->GetItem(_T("aaa147"))->Value.llVal, rowset->Fields->GetItem(_T("aaa148"))->Value.llVal ); rowset->MoveNext(); } CoUninitialize(); return 0; }ADO与OLEDB混合编程ADO相比较OLEDB来说确实方便了不少，但是它也有它的问题，比如它是封装的ActiveX控件，从效率上肯定比不上OLEDB，而且ADO中记录集是一次性将结果中的所有数据加载到内存中，如果数据表比教大时这种方式很吃内存。而OLEDB是每次调用GetNextRow时加载一条记录到内存（其实根据之前的代码可以知道它加载的时机，加载的大小是可以控制的），它相对来说比教灵活。其实上述问题使用二者的混合编程就可以很好的解决，在处理结果集时使用OLEDB，而在其他操作时使用ADO这样既保留了ADO的简洁性也使用了OLEDB灵活管理结果集内存的能力。在ADO中，可以通过_Recordset查询出ADORecordsetConstruction接口，这个接口提供了将记录集转化为OLEDB中结果集，以及将结果集转化为Recordset对象的能力下面是一个简单的例子CoInitialize(NULL); try { _bstr_t bsCnct(_T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;")); _RecordsetPtr Rowset(__uuidof(Recordset)); Rowset->Open(_T("select * from aa26;") ,bsCnct,adOpenStatic,adLockOptimistic,adCmdText); //获取IRowset接口指针 ADORecordsetConstruction* padoRecordsetConstruct = NULL; Rowset->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); IRowset * pIRowset = NULL; padoRecordsetConstruct->get_Rowset((IUnknown **)&pIRowset); padoRecordsetConstruct->Release(); DisplayRowSet(pIRowset); pIRowset->Release(); GRS_PRINTF(_T("\n\n显示第二个结果集:\n")); //使用OLEDB方法打开一个结果集 IOpenRowset* pIOpenRowset = NULL; TCHAR* pszTableName = _T("T_State"); DBID TableID = {}; CreateDBSession(pIOpenRowset); TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = (LPOLESTR)pszTableName; HRESULT hr = pIOpenRowset->OpenRowset(NULL,&TableID,NULL,IID_IRowset,0,NULL,(IUnknown**)&pIRowset); if(FAILED(hr)) { _com_raise_error(hr); } //创建一个新的ADO记录集对象 _RecordsetPtr Rowset2(__uuidof(Recordset)); Rowset2->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); //将OLEDB的结果集放置到ADO记录集对象中 padoRecordsetConstruct->put_Rowset(pIRowset); ULONG ulRow = 0; while(!Rowset2->EndOfFile) { COM_PRINTF(_T("|%10u|%10s|%-40s|\n") ,++ulRow ,Rowset2->Fields->GetItem("K_StateCode")->Value.bstrVal ,Rowset2->Fields->GetItem("F_StateName")->Value.bstrVal); Rowset2->MoveNext(); } } catch(_com_error & e) { COM_PRINTF(_T("发生错误:\n Source : %s \n Description : %s \n") ,(LPCTSTR)e.Source(),(LPCTSTR)e.Description()); } _tsystem(_T("PAUSE")); CoUninitialize(); return 0;这次就不再放上整体例子的链接了，有之前的基础应该很容易看懂这些，而且这次代码比较短，基本上将所有代码全粘贴了过来。