首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
139 阅读
2
nvim番外之将配置的插件管理器更新为lazy
98 阅读
3
2018总结与2019规划
69 阅读
4
从零开始配置 vim(15)——状态栏配置
58 阅读
5
PDF标准详解(五)——图形状态
46 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
elisp
文本编辑器
Java
读书笔记
反汇编
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
332
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
332
篇与
的结果
2018-08-19
毕业两年的反思
以下内容都是在醉酒状态下写的,各位就当看个热闹吧,千万不要当真,要真能给您一些启发那就谢天谢地了。到今年6月份,已经毕业两年了,在这两年中换过一家公司,从银行外包到安全行业,经历过加班,也经历过无所事事,心中有些感慨和想法与大家分享时间真的是不等人,之前刚入行时我也是由一位老大哥带着做项目,那个时候我差不多是项目组年龄最小的,而如今在现在的公司中,我成了唯二的老员工,我从去年入职现在的公司到现在公司经历了一轮大的换血,老员工差不多都走了,而现在我也从当初的被别人带转换了角色变成了带新人,新员工都是刚毕业的大学生,有志向,有抱负,有的也有能力,跟他们一比,真的有种长江后浪推前浪的感觉。心中不免有些惆怅,原来一年一年的混日子,时间过的这么快。现在我真正理解了作为程序员必须的具备终身学习的能力。以及那种随时都有可能被新人给淘汰掉的危机感第二个就是不能对技术太过于执着,现在很多程序员包括我在内总是唯技术,好像一些产品里面用了某种牛逼的技术那它就值钱,就牛逼。其实真的不是这样的,技术是为需求服务的,再好的技术也是为了解决现实中的问题,只要能解决现实问题就算你用最low的算法,它也是一个好的产品。从这个角度来说,一个程序员的薪资水平跟他自身的技术水平并不是正相关的关系,薪资水平高低在于他能够利用手中的技术为公司带来多大效益或者说它能为多少人产生多大的价值。技术就是这样没有最好的,也没有最坏的,没有什么所谓的最前言,也没有什么淘汰之说,只有最合适它的平台,就拿汇编来说,很多地方的确用不上汇编了,但是在某些场合,比如说做安全进行HOOK的时候可能会需要,或者在一些对性能有着变态需求的地方,有的实在不能再优化了,只有将C代码改为对应的汇编代码。没有最好的技术只有最合适的技术既然说到终身学习,那么下面就来说说程序员学习的事。学习首先得找一个方向,也就是经常说的职业规划,一般来说刚入行的程序员可能并不知道自己该从事何种方向,就知道一心写代码,公司让干啥就干啥,公司有啥业务就学啥业务。确实一般刚入行的新人可能对整个行业没有一个整体的把握。我觉得入行后可以花1到2年的时间来关注业界相关新闻,不断跳槽以便接触更多方向,然后从中选择一个适合自己的或者自己真正喜欢的方向,随后自己的精力需要完全投入到这个方向上来,学习的路线也应该围绕着这个方向来进行。比如说我现在从事Web安全的开发,刚开始也是什么都不懂,XSS、SQL注入等等完全不了解,这些涉及到的是Web的相关知识,因此针对这点我自己给的学习路线是,首先学习HTML、CSS这些前端的相关知识,然后学习Web开发、了解常见的一些框架和开发语言,比如PHP、Java等等,公司主要做的是一款扫描器,而扫描器中很重要的一个部分是爬虫,现在爬虫一般流行用Python,因此我又顺便学习了Python的相关内容,而扫描器用来检测漏洞的一些POC都是用Python写的,这样也必须的学习Python。而Web又是基于HTTP协议的因此必须得学习HTTP协议,顺便可以学习TCP/IP协议的整个架构。因此这样很容易就能理出一条学习的线路来。所以说在后续学习的时候需要找出自己真正喜欢的方向(不一定是喜欢,但是必须是自己不讨厌的),了解一下这个方向的主要内容,从中找出自己的不足来进行针对性的学习。然后可以尝试着利用学到的知识写点小工具等等,比如我自己常常使用Python搜集一些网上的POC来集成到自己的程序中用来制作简单的漏洞探测工具。我感觉现在整个IT行业存在一个浮夸、焦躁的环境。经常就是过段时间有一个新技术出来,然后大量的关于新技术的课程,文章,以及大量的招对应的开发人员,看着薪水好像挺高,似乎这个技术就是未来,其他的技术都过时了,而我们程序员很容易被这种风气给带歪了,他们会花时间去学习新技术,然后跳槽,然后从简历上看他似乎什么都做过,前端火的时候他是一个前端程序员,后来Python全栈火了,他又成了全栈,然后大数据火了,他又做大数据去了,而现在AI火了,然后他现在是一个AI的开发人员,似乎他什么都会,但是后来发现就一个调用框架然后复制粘贴。当然我不反对学习新技术,学习新技术应该与应用相结合或者说与现实中的问题相结合,比如做扫描器经常会有误报,怎么消除误报呢?AI是不是一个好方法,能不能用机器学习来减少误报?如果可行,那么就可以考虑去学习一下。还有我们如何从海量的日志信息中找出具有攻击行为的那些日志?这个可不可以利用大数据来进行检索?利用机器学习来准确识别?我觉得这才是学习新技术的真正原因,而不是光看重新行业的薪水。还是前面说的技术是为需求服务的,学习也是一样的,为现实中具体的需求来学习。归根到底,学习应该是为了用新技术来解决现实问题。在这两年中,我越来越觉得基础知识的重要性,当初在学校时总觉得很多东西会用就行,不需要纠结它的细节,什么快速排序,二叉树等等这些都有现成的库函数,没必要去了解它们,所以没有好好学习这些内容。现在看来这种看法很有问题,学习基础就好像当时很多人嘲讽买菜不用微积分那样觉得生活中用不到它们。现在也有很多人跟我说过,学习那些算法没有用,这些都由老外在写,人家那么大的组织机构来维护,你自己写的肯定效果上没有别人的好,到时候会用就行。但是我想如果作为一个程序员如果只是把自己限制在使用别人的库,只会复制粘贴别人的代码,这样永远是一个底层的码农,而做不了真正的程序员,还是那句话让它服务于具体的应用,比如你学习了HTTP协议之后,能不能仿照它,做一个能在嵌入式设备中使用的微型的HTTP服务器而不是直接上Apache,或者我能不能利用它的设计思想来提供类似的功能。而不是说为了使用HTTP而使用,学习这些算法与基础的意义并不在于会用别人写的库,而在于根据具体的情形选择并设计编写出适当的代码。或者能更好的使用这些开源库,而不是仅仅把别人demo的东西照搬过来。在当今互联网时代,学习东西比过去方便了许多,但是互联网也有许多问题,第一个就是互联网上资源太丰富了,以至于想学一个课程但是找不到合适的,比如说我想学下C语言,你会发现网上一大片C的内容,有视频有博客还有电子/实体书,但是各种资源的质量良莠不齐,很难抉择。经常听到有建议说自学能力强的选择看视频或者看书,但是对于初学者来说很难分辨一个资源的好坏,我当时自认为自学能力很强,在大学期间找了各种视频,看了各种书,吃过很多亏走过很多弯路,才学会了点C的皮毛,经常就是一个视频看完了,感觉基础学会了,想找一些实践的东西,然后发现很多都是前面一点快速把基础过一遍,然后开始做项目,最后发现跟着视频项目做出来了,但是并没有学到什么东西,反倒是丢了视频好像什么都不会,然后就会产生一定的挫败感。我自己当时也是被虐到怀疑人生也常常在思考我是不是适合这个行业。但是好在我从来没有在我应该学习C/C++这条路上产生过怀疑。说到这里,我想说说我对培训机构的看法,现在很多人说到培训机构总是一副喊打喊杀的语气,好像培训机构毁了整个IT行业。其实培训机构很不错的,他至少能带你入门,我们常说师傅领进门,修行在个人。跟着培训机构就好像有一个前辈一直在带你,给你分享他的见识,分享他的思路,而且培训机构能让你快速融入这个圈子,毕竟你的同学老师都是从事这个行业的。从这个角度来看好的培训结构能使你受用终身。每个行业都有好有坏,不要一杆子打倒一片。但是一般包就业,并承诺薪资的一般是坑。互联网的另一个坏处就是造就了一批伸手党,他们混迹于各大论坛,QQ 群,经常就是甩出一段代码然后告诉你这个跟他想的不一样,该怎么改。最可恨的是有的还给的是截图,你想给他调都没办法,难道要别人一个一个的照着敲,拜托大家都很忙好吧。其实很多问题都是你自己单步调试一下就可以解决的,很多人就是不愿意,一句我是初学者,我不会,所以你帮我看看,一般像这种看了真的是让人火大;另外一种人就是自认为是大神的,经常有初学者提问说,我想学下XXX,请问该怎么学,这个时候总有那么几个大神会告诉人家,你学这个没用,还不如去学XXX,或者说这个不用学,有现成的代码,然后甩一个连接给段代码,这种人也是令人火大的,人家初学者想学一个东西,你没有系统的学过就不要回答了,我觉得这样完全是误人子弟。所以你看:现在互联网虽然方便,但是学习的路并似乎没有变得轻松,它只是以另一种形式增加了你学习的成本而已,过去很多东西报个班或者找个老师花点钱就给你讲明白了,但是现在互联网时代很多人觉得资源应该共享,应该免费。但是从某种程度上学习成本其实基本没变,而且是以一种无形的成本来体现出来的,通常这种成本是最难发觉的,可能对你造成的误导是最深的。以上都是本人的一些突发奇想,写这些的时候是跟以前的同学一起喝过酒的,很多地方都有点胡言乱语的意思。请各位见谅。。。。。
2018年08月19日
9 阅读
0 评论
0 点赞
2018-08-12
WinSock Socket 池
之前在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 聚合与散播的代码全放在码云上了: 示例代码
2018年08月12日
5 阅读
0 评论
0 点赞
2018-08-04
C++ 调用Python3
作为一种胶水语言,Python 能够很容易地调用 C 、 C++ 等语言,也能够通过其他语言调用 Python 的模块。Python 提供了 C++ 库,使得开发者能很方便地从 C++ 程序中调用 Python 模块。具体操作可以参考: 官方文档在调用Python模块时需要如下步骤:初始化Python调用环境加载对应的Python模块加载对应的Python函数将参数转化为Python元组类型调用Python函数并传入参数元组获取返回值根据Python函数的定义解析返回值初始化在调用Python模块时需要首先包含Python.h头文件,这个头文件一般在安装的Python目录中的 include文件中,所在VS中首先需要将这个路径加入到项目中包含完成之后可能会抱一个错误:找不到 inttypes.h文件,在个错误在Windows平台上很常见,如果报这个错误,需要去网上下载对应的inttypes.h文件然后放入到对应的目录中即可,我这放到VC的include目录中在包含这些文件完成之后可能还会抱一个错误,未找到Python36_d.lib 在Python环境中确实找不到这个文件,这个时候可以修改pyconfig.h文件,将这个lib改为python36.lib,具体操作请参考这个链接: https://blog.csdn.net/Chris_zhangrx/article/details/78947526还有一点要注意,下载的Python环境必须的与目标程序的类型相同,比如你在VS 中新建一个Win32项目,在引用Python环境的时候就需要引用32位版本的Python这些准备工作做完后在调用Python前先调用Py_Initialize 函数来初始化Python环境,之后我们可以调用Py_IsInitialized来检测Python环境是否初始化成功下面是一个初始化Python环境的例子BOOL Init() { Py_Initialize(); return Py_IsInitialized(); }调用Python模块调用Python模块可以简单的调用Python语句也可以调用Python模块中的函数。简单调用Python语句针对简单的Python语句(就好像我们在Python的交互式环境中输入的一条语句那样),可以直接调用 PyRun_SimpleString 函数来执行, 这个函数需要一个Python语句的ANSI字符串作为参数,返回int型的值。如果为0表示执行成功否则为失败void ChangePyWorkPath(LPCTSTR lpWorkPath) { TCHAR szWorkPath[MAX_PATH + 64] = _T(""); StringCchCopy(szWorkPath, MAX_PATH + 64, _T("sys.path.append(\"")); StringCchCat(szWorkPath, MAX_PATH + 64, lpWorkPath); StringCchCat(szWorkPath, MAX_PATH + 64, _T("\")")); PyRun_SimpleString("import sys"); USES_CONVERSION; int nRet = PyRun_SimpleString(T2A(szWorkPath)); if (nRet != 0) { return; } }这个函数主要用来将传入的路径加入到当前Python的执行环境中,以便可以很方便的导入我们的自定义模块函数首先通过字符串拼接的方式组织了一个 "sys.path.append('path')" 这样的字符串,其中path是我们传进来的参数,然后调用PyRun_SimpleString执行Python的"import sys"语句来导入sys模块,接着执行之前拼接的语句,将对应路径加入到Python环境中调用Python模块中的函数调用Python模块中的函数需要执行之前说的2~7的步骤加载Python模块(自定义模块)加载Python的模块需要调用 PyImport_ImportModule 这个函数需要传入一个模块的名称作为参数,注意:这里需要传入的是模块的名称也就是py文件的名称,不能带.py后缀。这个函数会返回一个Python对象的指针,在C++中表示为PyObject。这里返回模块的对象指针然后调用 PyObject_GetAttrString 函数来加载对应的Python模块中的方法,这个函数需要两个参数,第一个是之前获取到的对应模块的指针,第二个参数是函数名称的ANSI字符串。这个函数会返回一个对应Python函数的对象指针。后面需要利用这个指针来调用Python函数获取到函数的指针之后我们可以调用 PyCallable_Check 来检测一下对应的对象是否可以被调用,如果能被调用这个函数会返回true否则返回false接着就是传入参数了,Python中函数的参数以元组的方式传入的,所以这里需要先将要传入的参数转化为元组,然后调用 PyObject_CallObject 函数来执行对应的Python函数。这个函数需要两个参数第一个是上面Python函数对象的指针,第二个参数是需要传入Python函数中的参数组成的元组。函数会返回Python的元组对象,这个元组就是Python函数的返回值获取到返回值之后就是解析参数了,我们可以使用对应的函数将Python元组转化为C++中的变量最后需要调用 Py_DECREF 来解除Python对象的引用,以便Python的垃圾回收器能正常的回收这些对象的内存下面是一个传入空参数的例子void GetModuleInformation(IN LPCTSTR lpPyFileName, OUT LPTSTR lpVulName, OUT long& level) { USES_CONVERSION; PyObject *pModule = PyImport_ImportModule(T2A(lpPyFileName)); //加载模块 if (NULL == pModule) { g_OutputString(_T("加载模块[%s]失败"), lpPyFileName); goto __CLEAN_UP; } PyObject *pGetInformationFunc = PyObject_GetAttrString(pModule, "getInformation"); // 加载模块中的函数 if (NULL == pGetInformationFunc || !PyCallable_Check(pGetInformationFunc)) { g_OutputString(_T("加载函数[%s]失败"), _T("getInformation")); goto __CLEAN_UP; } PyObject *PyResult = PyObject_CallObject(pGetInformationFunc, NULL); if (NULL != PyResult) { PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0); PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1); //获取漏洞的名称信息 int nStrSize = 0; LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize); StringCchCopy(lpVulName, MAX_PATH, pVulName); PyMem_Free(pVulName); //获取漏洞的危险等级 level = PyLong_AsLong(pVulLevelObj); Py_DECREF(pVulNameObj); Py_DECREF(pVulLevelObj); } //解除Python对象的引用, 以便Python进行垃圾回收 __CLEAN_UP: Py_DECREF(pModule); Py_DECREF(pGetInformationFunc); Py_DECREF(PyResult); }在示例中调用了一个叫 getInformation 的函数,这个函数的定义如下:def getInformation(): return "测试脚本", 1下面是一个需要传入参数的函数调用BOOL CallScanMethod(IN LPPYTHON_MODULES_DATA pPyModule, IN LPCTSTR lpUrl, IN LPCTSTR lpRequestMethod, OUT LPTSTR lpHasVulUrl, int BuffSize) { USES_CONVERSION; //加载模块 PyObject* pModule = PyImport_ImportModule(T2A(pPyModule->szModuleName)); if (NULL == pModule) { g_OutputString(_T("加载模块[%s]失败!!!"), pPyModule->szModuleName); return FALSE; } //加载模块 PyObject *pyScanMethod = PyObject_GetAttrString(pModule, "startScan"); if (NULL == pyScanMethod || !PyCallable_Check(pyScanMethod)) { Py_DECREF(pModule); g_OutputString(_T("加载函数[%s]失败!!!"), _T("startScan")); return FALSE; } //加载参数 PyObject* pArgs = Py_BuildValue("ss", T2A(lpUrl), T2A(lpRequestMethod)); PyObject *pRes = PyObject_CallObject(pyScanMethod, pArgs); Py_DECREF(pArgs); if (NULL == pRes) { g_OutputString(_T("调用函数[%s]失败!!!!"), _T("startScan")); return FALSE; } //如果是元组,那么Python脚本返回的是两个参数,证明发现漏洞 if (PyTuple_Check(pRes)) { PyObject* pHasVul = PyTuple_GetItem(pRes, 0); long bHasVul = PyLong_AsLong(pHasVul); Py_DECREF(pHasVul); if (bHasVul != 0) { PyObject* pyUrl = PyTuple_GetItem(pRes, 1); int nSize = 0; LPWSTR pszUrl = PyUnicode_AsWideCharString(pyUrl, &nSize); Py_DECREF(pyUrl); StringCchCopy(lpHasVulUrl, BuffSize, pszUrl); PyMem_Free(pszUrl); return TRUE; } } Py_DECREF(pRes); return FALSE; }对应的Python函数如下:def startScan(url, method): if(method == "GET"): response = requests.get(url) else: response = requests.post(url) if response.status_code == 200: return True, url else: return FalseC++数据类型与Python对象的相互转化Python与C++结合的一个关键的内容就是C++与Python数据类型的相互转化,针对这个问题Python提供了一系列的函数。这些函数的格式为PyXXX_AsXXX 或者PyXXX_FromXXX,一般带有As的是将Python对象转化为C++数据类型的,而带有From的是将C++对象转化为Python,Py前面的XXX表示的是Python中的数据类型。比如 PyUnicode_AsWideCharString 是将Python中的字符串转化为C++中宽字符,而 Pyunicode_FromWideChar 是将C++的字符串转化为Python中的字符串。这里需要注意一个问题就是Python3废除了在2中的普通的字符串,它将所有字符串都当做Unicode了,所以在调用3的时候需要将所有字符串转化为Unicode的形式而不是像之前那样转化为String。具体的转化类型请参考Python官方的说明。上面介绍了基本数据类型的转化,除了这些Python中也有一些容器类型的数据,比如元组,字典等等。下面主要说说元组的操作。元组算是比较重要的操作,因为在调用函数的时候需要元组传参并且需要解析以便获取元组中的值。创建Python的元组对象创建元组对象可以使用 PyTuple_New 来创建一个元组的对象,这个函数需要一个参数用来表示元组中对象的个数。之后需要创建对应的Python对象,可以使用前面说的那些转化函数来创建普通Python对象,然后调用 PyTuple_SetItem 来设置元组中数据的内容,函数需要三个参数,分别是元组对象的指针,元组中的索引和对应的数据示例: PyObject* args = PyTuple_New(2); // 2个参数 PyObject* arg1 = PyInt_FromLong(4); // 参数一设为4 PyObject* arg2 = PyInt_FromLong(3); // 参数二设为3 PyTuple_SetItem(args, 0, arg1); PyTuple_SetItem(args, 1, arg2);或者如果元组中都是简单数据类型,可以直接使用 PyObject* args = Py_BuildValue(4, 3); 这种方式来创建元组解析元组Python 函数返回的是元组,在C++中需要进行对应的解析,我们可以使用 PyTuple_GetItem 来获取元组中的数据成员,这个函数返回PyObject 的指针,之后再使用对应的转化函数将Python对象转化成C++数据类型即可PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0); PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1); //获取漏洞的名称信息 int nStrSize = 0; LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize); StringCchCopy(lpVulName, MAX_PATH, pVulName); PyMem_Free(pVulName); //释放由PyUnicode_AsWideCharString分配出来的内存 //获取漏洞的危险等级 level = PyLong_AsLong(pVulLevelObj); //最后别忘了将Python对象解引用 Py_DECREF(pVulNameObj); Py_DECREF(pVulLevelObj); Py_DECREF(PyResult);Python中针对具体数据类型操作的函数一般是以Py开头,后面跟上具体的数据类型的名称,比如操作元组的PyTuple系列函数和操作列表的PyList系列函数,后面如果想操作对应的数据类型只需要去官网搜索对应的名称即可。这些代码实例都是我之前写的一个Demo中的代码,Demo放到了Github上: PyScanner
2018年08月04日
5 阅读
0 评论
0 点赞
2018-07-21
WinSock2 API
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真正做到了与协议无关。
2018年07月21日
3 阅读
0 评论
0 点赞
2018-07-14
《人类简史》与《未来简史》 读后感
我也忘了之前在干嘛的时候找到了这本书,当时网上对它的评价非常高,认为它从一个新的高度来阐述了人类历史,人类的发展脉络,很多人都赞誉作者的博学多才,称作者在许多领域都有涉及,当初我抱着很大的希望买了人类简史与未来简史这两本书,但是结果确不那么令人满意,如果说满分100分的话,我可能最多给75分。书中确实有许多有趣的理论,也有许多我看了十分称道的地方,但是也有许多我不认同的地方也有许多我感觉是老生常谈的东西。下面我想把里面有趣的地方和我不太喜欢的地方列举出来。总体脉络我觉得从人类简史看到未来简史将是一部十分有趣的画卷。作者基本上从智人的诞生说到了智人最后的消亡。从整体脉络上看首先谈到了智人的诞生,从认知革命开始,智人开始慢慢有了不同于其他人种的能力,他们有了八卦、讲述虚拟故事的能力,根据这些能力,拥有了比其他人种更加优秀的组织能力,利用这一能力,智人消灭了其他人种。紧接着,智人产生了农业革命,拥有了过剩的粮食,从而能够养活那些不从事农业的人口,这样人类渐渐的产生了不同的分工从而诞生了专门的人才,这样就产生了阶级、帝国等等。接着产生了工业革命,造成了人类物产的极大丰富。因为某些原因最后产生了新的一场科技革命,然后人类利用科技的力量能够改造生物,最终会加速自身的进化更好的适应环境,最终可能在某个时间以后新进化的新人类与现在的智人已经是两个不同的人种,这也就意味着智人正式推出历史舞台。我感觉本书最大的特色是它作为一本历史类的书并没有局限于某一个朝代,某一些人物,而是站在宏观的角度,从智人诞生之日说到智人的未来,作者就像站在外太空冷静的观察这地球上名为人类这个物种从产生到现代的各种演化,就好像我打通关了文明这款游戏。作者应该是打通关了智人这款类似的游戏。认知革命书中提到大约就是在距今7万到3万年前,人类基因可能发生某些突变导致大脑中的某些神经元连接发生变化,于是人类产生了一项新的能力,也就是讲述虚构故事的能力以及八卦的能力,作者在书中提到在采集社会里面,男性采集者一般都是外出打猎,他们需要集中注意力在猎物身上,所以这也就注定了男性能够长时间集中注意力在某一点上,而女性一般需要采集或者在洞中养儿育女,特别是在采集的时候需要放眼一片,所以女性天生的注意力不如男性集中;而且相比于男性需要长期在外打猎来说,女性在部落的时间要长,在部落的一些事都逃不脱她们的眼睛,这样她们就给了她们八卦的材料,谁偷了谁的东西,谁被戴了绿帽子等等破事就成了她们在洞中消遣、八卦的话题,这些都是认知革命赋予的能力;当然作者也提到女性这些八卦的能力有助于部落健康成功,借助女性的八卦,能让其他人迅速分辨部落中的坏人。通过认知革命,人类会相信虚构的事物,从而产生了各种原始图腾的崇拜,由于不同的家族或者部落可能崇拜相同的图腾这样就可以迅速集结大量崇拜相同图腾的人力,这样相比于其他人种以血缘关系来维持的部落来说占据了大量的人口优势。而且由于能够讲述不曾发生的事,这给知识传播创造了条件,这就使智人相比于其他人种来说占据了绝对的优势。因此其他人在智人的扩张中逐渐被智人取代。认知革命是书中的亮点之一,可以说整本书甚至整个人类文明就是建立在认知革命的基础之上,没有对虚拟事务的讲述,对不存在事物的认同,也就不会产生货币,知识,宗教,国家,公司等等概念。智人拥有的这种能够迅速聚集和组织大量人力完成各种大型的工作,这个才是人类最宝贵的能力,人类文明可以被毁灭,但是只要人类保留这个能力很快就又能建立起新的文明。农业革命大致在某个时间段,人类开始慢慢的放弃了采集狩猎的生活转而开始了日出而作,日落而息的农耕生活。作者在这部分的开篇叫做史上最大的骗局,认为人类进入到了农耕文明是一个巨大的退步。书中说,人类由于在农耕时需要长期弯腰劳作导致人类在后续会出现腰肌劳损等一系列疾病,同时由于农耕人口密集,而且都是与各种家畜动物一起,所以经常发生各种传染病和瘟疫,而在采集社会没有这种情况;并且我在读书的时候总感觉作者一直在歌颂原始的采集社会是如何好,风景优美,人与动物和谐相处,人累了就休息,饿了就找点野果子或者去打打猎,而且那个时候到处都有各色的野果,各种动物,食物的多样性能使那个时候的人营养均衡。而农业社会只有简单的主食,容易造成营养失衡。而且还需要看天吃饭,哪年收成不好就会出现饥荒。这些似乎在原始采集社会都不会看到。看到这些我不经想起过去的一些诗人总爱歌颂田园时代,认为牧童骑黄牛吹小曲,老人垂钓是一副很美的画面,似乎人人都无忧无虑。但是真的是无忧无虑吗? 《理性乐观派》这本书告诉我,牧童可能饿着肚子好几天没吃饭,恨不得吃力他骑的大黄牛,而老人可能也是因为找不到吃的而无奈选择垂钓,他可能并不是优哉游哉的钓鱼,而是恨不得把池塘的水弄干赶快弄几条鱼充充饥。我们总会被一些眼前所看到的画面给糊弄,然后编制根据自己的经验给看到的画面赋予美好的诗意,但是要仔细想想那些能够流传下来诗句的都不是普通人,他们不愁吃喝,他们完全想不到底层人民的生活是有多辛苦;他们自己衣食无忧,偶尔出去串亲访友,郊外踏青,以自己当时的心情写出来的诗句,写出来他看到的画面当然是一副诗情画意的画卷。但是“子非鱼,焉知鱼之乐”。因此这里我认为作者只是一厢情愿的认为农业社会不如采集社会,认为采集社会多么多么好,作者并没有看到,由于人类步入农业社会能够产生更多粮食,从而产生了分工,慢慢发展成了现在的样子。而作者所说的那些采集社会的美好事物我觉得并不存在,如果采集社会真的那么诗情画意,生在采集社会的人类那么悠闲为什么没有发展出众多手工艺人,科学家;没有直接产生工业社会,人类在采集社会那么多年(至少有几万年),而人类文明发生却是在最近几千年之内?产生这些的原因我认为是采集社会不如农耕社会,它没有农耕社会的人口多,它不能产生足够的粮食,每个人没有办法脱离打猎采集,每个人都必须收集粮食而没有时间做其他事情。而且相对于农业社会能够稳定的获取粮食来源来说,采集社会食物来源虽然多样,但是有一定的不确定性和危险性,采集社会的人类需要经常迁徙到不同的地方。而农业社会一般都会定居。相比较于农业社会的定居来说,采集社会在迁徙到另一个地方的时候,部族中的老弱病残必定成为队伍的负担,书中也提到在迁徙的过程中会有专门的人来处理这些老弱病残的人,一般都是残忍的将其杀害;而在农业社会则不必这样,老人一般都是最有经验的人,他们会向子女传授种地的经验,而且他们靠自己的子女就能养活自己。这也是农业社会优于采集社会的一个标志。另外我也不觉得农业社会的营养单一,农业社会有各种主食,种植各种蔬果,饲养各种家畜,他们的食物的多样性并不比采集社会差。因此我一直相信人类社会是一直进步的,不存在什么过去比现在要好的这种论点,而且说实在的我很反感各种怀念过去的书。当然这些问题并不影响这本书精彩的地方,比如他里面也提到农业社会相对于采集社会来说,每相同面积的土地能养活更多人,如果单从基因角度来讲,农业社会是优于采集社会(仅仅是从基因角度吗?我觉得从各个方面来看农业社会都是一大进步)。而且书中提到如果单从基因来说农业社会看似是人类的胜利,但是也可能是各种农作物,家畜的胜利。比如“小麦驯化了人类,”——在一万年前,小麦也不过就是野草当中的一种,只出现在中东一个很小的地区,但就在短短1000年内,小麦忽然就传遍了世界各地,生存和繁衍正是最基本的演化标准,而根据这个标准,小麦可以说是地球史上最成功的植物。而小麦成功的秘诀就在于操纵智人为其所用,给自己大规模创造适宜的生存环境。而人类却为了适应小麦和小麦农业,却付出了无数健康、劳动、营养、人身安全方面的代价。从这个角度来看确实很有道理。哈哈!后续几章其实我感觉从看完农业革命开始,我对这本书的好感基本上已经没有多少了,很多地方也没有仔细的读。所以也不太熟悉。对作者也没有太多的好感了。确实得承认作者涉猎许多方面,对许多学科都有研究。但是给我感觉就是作者毕竟只是一个历史学家,书中充斥着对人类的批判,似乎现在批判人类的暴行,呼吁保护其他的动物的利益已经成了政治正确了。书中使用大量的例子证明人类采用圈养的方式,特别是现代农业机械化的饲养动物使得各种动物十分难受,是十分不人道的行为。这种东西可能我在读高中的时候会比较认同,但是从现在来看,特别是我大学读过一点书之后,我就不怎么支持这个观点了。从某种程度上来说,动物只是人类的财产,虽然它们可能与人类相似,有各种感官,有七情六欲,但它们终归只是人类的财产,人类只是在合理的利用财产产生财富,造福于人,这并没有什么过错。正是由于这种工业化的方式才养活了众多人口,产生大量资源。让人类能够以极少的人口产生大量的粮食来养活各种专门的人才,才会有现代社会的繁华。说点题外话,在很多人的认识里,认为工业化是在过渡向自然索取,是在破坏环境。但是从另一方面想,工业社会能够以极少的资源产生大量的物资,以少部分人的劳动力能满足大部分人的衣食住行。也正是工业社会造成人力向大城市流动,而使农村人口减少,这样给农村的自然环境的恢复提供了很好的提交。而且工业能极大的提高材料的利用率,从而导致对应原材料的种植面积降低,从而使大量农田荒废,这些荒废的农田能够转化为自然的一部分,这些都有利于环境的恢复。所以说从各种方面来说我认为社会是一直进步的。不要因为害怕对环境造成的影响而放弃发展科技和工业,其实在未来现在的一些问题可能迎刃而解。人类的未来说完了人类简史,再来谈谈我对未来简史这本书的看法,其实我自己感觉这本书比人类简史写的要精彩一些,但是它最大的问题是里面仍然有大量讲述历史,讲述人类过去的内容,可能作者觉得需要利用历史来看未来。这些可能导致我自己感觉这两本书有大量重复的内容。人类简史相比较于未来简史只是从不同的角度来讲述历史罢了。未来简史其实只有简短的几章内容在讲述未来,而这部分也是我最喜欢的,也是这两本书最值的地方首先作者认为通过生物技术,人类可以改造自身,特别是在完全破解人类基因组的秘密之后。人类可能会利用基因技术将自身改造的更加适应这个环境。而且改造自身不一定要通过生物技术,使用仿生的机械技术同样可以改造人类,未来随着人类器官的病变衰老,为了治疗必定会出现使用机械代替原始器官的操作,到时候会不会出现五脏六腑都是机械而意识确是人类的生物呢?其实我有店小期待。这样人类就已经变成新的物种了。可能从某个时候人类就能实现永生了。作者认为人类会利用科学技术还会达到这样几个目标,永生、永远幸福等等,针对幸福的定义,任何人都没有明确的答案,但是作者认为幸福或者愉快的感觉其实是人类体内某些激素产生的结果,如果利用生物技术未来是不是我们可以人工产生某种激素然后通过某种方式来作用于人体而使人产生快乐的感觉呢?我觉得这种方式与那些毒品很相像,如果真的要靠那种方式快乐,还不如不要快乐和幸福。书中作者也通过各种数据和实验让我想到其实人类只是一个化学容器,人类体内时刻在发生各种化学反应,产生各种激素,各种生物电流与激素让我们产生各种感觉,像伤心,愉快,幸福,生气等等。各种激素的含量会影响人类的情绪,甚至影响健康。而且人类的各种器官,人类的各种行为也会产生数据,未来可能在人类体内有各种探测器,根据探测得到的各种数据提出各种健康建议,未来生病了医生可能直接调取探测器的各种数据就可以明确作出判断。这从某种方面证明了数学的强大。既然一切都是数据,那么人类的各种行为是不是可能存在一个公式利用人类体内激素或者其他数据来推测人类下一步行动,以后会不会产生向电影里面演的一样,在天上存在一个巨大的天眼系统,如果侦测到某个人即将有犯罪行为或者正在进行犯罪行为时,会根据产生的危害选择自动通知公安机关抓人或者选择直接击毙,毕竟如果将来出现靠谱的算法,可能判断的准确率能达到90%以上。真是细思极恐!书中另外提到未来人类可能会通过对细胞的改造实现永生,或者使自己变得比其他人优秀,或者通过胚胎或者其他技术来修改新生儿的基因以便得到更加优秀的后代。如果后续真的有这项技术,会不会造成更加的不平等?有钱人有更多的钱以便改造出更加强大的后代,而屌丝的后代永远是没有经过改造的屌丝。这样就造成了永远的阶级固化,会不会引起阶级矛盾?而针对永生技术来说后续即使有能力能够达到,会不会造成不平等?有钱人能够永远活着而穷人只有卑贱的死去?当时我产生了一个想法,如果政府运行将永生技术商业化,会不会出现这样的情形,会出现一批技术比较牛的公司转本为富人服务,能够使富人一次性获得几百年的寿命;同时市场上出现一些可能不那么有技术的公司,它们专门服务与穷人,一次收取少量费用,为穷人增加几年到几十年的寿命;这两类公司哪一类比较挣钱?我的想法是服务于穷人的公司会成为市值最高的公司,从舆论上将,这类公司体现的公平,人民群众会比较支持这类公司,而且拥有大量财富的人只有少部分人,而从普通人那里挣钱可以做到薄利多销,可能最终会称霸整个行业。就像当年的通用汽车和微软一样。我感觉服务与底层人民群众的公司才是真正挣钱的公司,毕竟市场在那。但是可能这种公司最早肯定是需要为富人服务的,这样明显造成不公的技术应该从开始就不会商用。但是从市场的角度来看我还是希望它能够商用,毕竟不管它早期怎么服务与富人,最后一定会有公司来瞄准普通阶级,哪怕质量差点,毕竟市场摆在那。以上很大一部分都只是我的个人脑洞。暂时就说到这吧,其实这两本书我反而觉得后面的未来简史更吸引我,在读的时候未来简史让我产生了许多有趣的脑洞,这里有的地方我也想不起来了,以后如果记起来了可能会补充,今天就先到这;最后说一句,虽然我不太喜欢人类简史这本书,但是仁者见仁,还是推荐一波吧,最后我给个75分吧
2018年07月14日
4 阅读
0 评论
0 点赞
2018-07-06
WinSock 完成端口模型
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的上述问题,提出了完成端口的解决方案,完成事件由对应的线程处理,而主线程只需要专注于它自己的工作就好了,这就相当于警报响了,我们知道信来了,直接派一个人去拿信,后面的我就不管了,而拿信的人把信拿回来的时候将信放好。当我们忙完之后去处理这封信。没忙完的话信就一直放在那,甚至让拿信的人处理这封信,这样就能更高效的集中注意力来处理眼前的工作。
2018年07月06日
3 阅读
0 评论
0 点赞
2018-06-29
WinSock 重叠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则需要关闭连接,否则原样返回并准备下一次接收数据
2018年06月29日
7 阅读
0 评论
0 点赞
2018-06-23
WinSock WSAEventSelect 模型
在前面我们说了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 WSASendTo| |FD_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,看看是不是有待决的,当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求最后,整个示例代码
2018年06月23日
5 阅读
0 评论
0 点赞
2018-06-16
为什么C语言会有头文件
前段时间一个刚转到C语言的同事问我,为什么C会多一个头文件,而不是像Java和Python那样所有的代码都在源文件中。我当时回答的是C是静态语言很多东西都是需要事先定义的,所以按照惯例我们是将所有的定义都放在头文件中的。事后我再仔细想想,这个答案并不不能很好的说明这个问题。所以我在这将关于这个问题的相关内容写下来,希望给大家一点提示,也算是一个总结include语句的本质要回答这个问题,首先需要知道C语言代码组织问题,也就是我比较喜欢说的多文件,这个不光C语言有,几乎所有的编程语言都有,比如Python中使用import来导入新的模块,而C中我们可以简单的将include等效为import。那么问题来了,import后面的模块名称一般是相关类和对象的的的声明和实现模块,而include后面只能跟一个头文件,只有声明。其实这个认识是错误的,C语言并没有规定include只能包含头文件,include的本质是一个预处理指令它主要的工作是将它后面的相关文件整个拷贝并替换这个include语句,比如下面一个例子//add.cpp int add(int x, int y) { return x + y; } //main.cpp #include "add.cpp" int main() { int x = add(1, 2); return 0; }在这个例子中我们在add.cpp文件中先定义一个add函数,然后在main文件中先包含这个源代码文件,然后在main函数中直接调用add函数,项目的目录结构如下:在这里给大家说一个技巧,在VS中右击项目--->选择属性------>C++------>命令行,在编辑框中填入 /P,然后打开对应的文件点击编译(这里不能选生成,由于/P选项只会进行预处理并编译这一个文件,其余.cpp文件并没有编译,选生成一定会报错)点击编译以后它会在项目的源码目录下生成一个与对应cpp同名的.i文件,这个文件是预处理之后生成的源文件。这个技巧对于调试检查和理解宏定义的代码十分重要,我们看到预处理之后的代码如下:int add(int x, int y) { return x + y; } int main() { int x = add(1, 2); return 0; }这段代码中我把注释给删掉了,注释表示后面的代码段都是来自于哪个文件的,从代码文件来看,include被替换掉了,正是用add.cpp文件中的代码替换了,去掉之前添加的/P参数,再次点击编译,发现它报错了,报的是add函数重复定义。因为编译add.cpp时生成的add.obj中有函数add的定义,而在main文件中又有add函数的定义。我们将代码做简单的改变就可以解决这个问题,最终的代码如下://add.cpp int add(int x, int y); #ifndef __ADD_H__ int add(int x, int y) { return x + y; } #endif // __ADD_H__ //main.cpp #define __ADD_H__ #include "add.cpp" int main() { int x = add(1, 2); return 0; }在这段代码中加了一个宏定义,如果没有定义这个宏则包含add的实现代码,否则不包含。然后在main文件中定义这个宏,表示在main中不包含它的实现,但是不管怎么样都需要在add.cpp中加上add函数的定义,否则在调用add函数时会报add函数未定义的变量或者函数上述写法的窘境上面只引入一个文件,我们来试试引入两个, 在这个项目中新增一个mul文件来编写一个乘法的函数#define __ADD_H__ #include "add.cpp" int mul(int x, int y); #ifndef __MUL_H__ int mul(int x, int y) { int res = 0; for(int i =0; i < y; i++) { res = add(res, x); } return res; } #endif上面的乘法函数利用之前的add函数,乘法是多次累加的结果,在上面的代码中由于要使用add函数,所以先包含add.cpp文件,并定义宏保证没有重复定义,然后再写对应的算法。最后在main中引用这个函数#define __ADD_H__ #define __MUL_H__ #include "add.cpp" #include "mul.cpp" int main() { int x = add(1, 2); x = mul(x, 2); return 0; }注意这里对应宏定义和include的顺序,稍有不慎就可能会报错,一般都是报重复定义的错误,如果报错还请使用之前介绍的/P选项来排错到这里是不是觉得这么写很麻烦?其实我在准备这些例子的时候也是这样,很多时候没有注意相关代码的顺序导致报错,而针对重复定义的报错很难排查。而这还仅仅只引入了两个文件,一般的项目中几时上百个文件那就更麻烦了头文件的诞生从上面的两个例子来看,其实我们只需要包含对应的声明,不需要也不能包含它的实现。很自然的就想到专门编写一个文件来包含所有的定义,这样要使用对应的函数或者变量的时候直接包含这个文件就可以了,这个就是我们所说的头文件了。至于为什么叫做头文件,这只是一个约定俗成的叫法,而以.h来命名也只是一个约定而已,我们经常看到C++的开源项目中将头文件以.hpp命名。这个真的只是一个约定而已,我们也看到了上面的例子都包含的是cpp文件,它也能编译过。其实针对所有的变量、类、函数可以都在统一的头文件中声明,但是这么做又带来一个问题,如果我要看它的实现怎么办,那么多个文件我不可能一个个的找吧。所以这里又有一条约定,每个模块都放在统一的cpp文件中而该文件中相关内容的声明则放到与之同名的头文件中其实我觉得这个原则在所有静态的、需要区分声明和实现的语言应该是都适用的,像我知道的汇编语言,特别是win32 的宏汇编,它也有一个头文件的思想。C语言编译过程在上面我基本上回答了为什么需要一个头文件,但是本质的问题还是没有解决,为什么像Python这类动态语言也有对应模块、多文件,但是它不需要像C那样要先声明才能使用?要回答这个问题需要了解一点C/C++的编译过程。C/C++编译的时候先扫描整个文件有没有语法错误,然后将C语句转化为汇编,当碰到不认识的变量、类、函数、对象的命名时,首先查找它有没有声明,如果没有声明直接报错,如果有,则根据对应的定义空出一定的存储空间并进行相关的指令转化:比如给变量赋值时会转化为mov指令并将、调用函数时会使用call指令。这样就解释了为什么在声明时指定变量类型,如果编译器不知道类型就不知道该用什么指令来替换C代码。同时会将对应的变量名作为符号保留。然后在符号表(这个符号表时每个代码文件都有一个)中填入该文件中定义的相关内容的符号以及它所在的首地址。最终如果未发生错误就生成了一个对应的.obj文件,这就是编译的基本过程。编译完成之后进行链接,首先扫描所有的obj文件,先查找main函数,然后根据main函数中代码的执行流程来一一组织代码结构,当碰到之前保留的符号时,去所有的obj中的符号表中根据变量符号查找对应的地址,当它发现找到多个地址的时候就会报重复定义的错误。如果未找到对应的符号就会报函数或者变量已经声明但是未定义。找到之后会将之前obj中的符号替换为地址,比如将 mov eax num替换成 mov eax, 0x00ff7310这样的指令。最终生成一个PE文件。根据上面的编译过程来看,它事先会扫描文件中所有的变量定义,所以必须让编译器知道这个变量是什么。而Python是边解释边执行,所以事先不需要声明,只要执行到该处能找到定义即可。它们这点区别就解释了为什么C/C++需要声明而Python不用。
2018年06月16日
4 阅读
0 评论
0 点赞
2018-06-03
WSAAsyncSelect 消息模型
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是否在对应数组中的操作,减少了循环。而且可以很好的把握什么调用时机问题。 主要的缺点是它需要一个窗口,这样在服务程序中基本就排除掉了这个模型,它基本上只会出现在客户端程序中。 另外如果在一个窗口中需要管理成千上万个句柄时,它的性能会急剧下降,因此它的伸缩性较差。但是在客户端中基本不存在这个问题,所以如果要在客户端中想要减少编程难度,它是一个不二的选择
2018年06月03日
7 阅读
0 评论
0 点赞
1
...
20
21
22
...
34