首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
113 阅读
2
nvim番外之将配置的插件管理器更新为lazy
83 阅读
3
2018总结与2019规划
64 阅读
4
PDF标准详解(五)——图形状态
42 阅读
5
为 MariaDB 配置远程访问权限
34 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
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
elisp
linux
文本编辑器
Java
反汇编
读书笔记
OLEDB
数据库编程
数据结构
Masimaro
是金子总会发光的,可你我都是老铁
累计撰写
319
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
319
篇与
的结果
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 点赞
2018-06-02
Facebook爬虫
初次接触到scrapy是公司要求编写一个能够解析JavaScript的爬虫爬取链接的时候听过过,当时我当时觉得它并不适合这个项目所以放弃这个方案,时隔一年多公司有了爬取Facebook用户信息的需求,这样才让我正式接触并使用到scrapy需求首先从文件或者数据库导入第一批用户做为顶层用户,并爬取顶层用户好友的发帖信息包括其中的图片将第一步中爬取到的用户好友作为第二层用户并爬取它们的发帖信息和好友信息将第二层用户中爬到的好友作为第三层用户并爬取它们的好友信息也就是说不断爬取用户的好友和它的发帖信息直到第三层为止根据这个需求首先来确定相关方案爬虫框架使用scrapy + splash:Facebook中大量采用异步加载,如果简单收发包必定很多内容是解析不到的,因此这里需要一个JavaScript渲染引擎,这个引擎可以使用selenium + chrome(handless) 这套,但是根据网上一位老哥的博客我知道了splash这种东西,在做相关比较之后我选择了使用splash,主要的理由有以下几点:a. 与selenium比较起来,它的官方文档更为全面b. 支持异步的方式,这个可以与scrapy的异步回调方式完美结合并充分发挥性能c. 它提供了一套与scrapy结合的封装库,可以像scrapy直接yield request对象即可,使用方式与scrapy类似降低了学习成本d. 它提供了lua脚本的方式可以很方便的操作浏览器对象e. 跨平台。相比于使用chrome作为渲染工具,它可以直接执行在Linux平台在scrapy中使用splash时可以安装对应的封装库scrapy_splash,这个库的安装配置以及使用网上基本都有详细的讲解内容,这里就不再提了当然它也有缺点,但是这并不在讨论之中,至于具体如何选择就是一个见仁见智的问题了开发语言: python3 ,python在开发爬虫方面有独特的优势,这个就不用我多说了,弄过爬虫的朋友都知道开发工具 pycharm, JB的pycharm几乎是Python IDE的首选设计与实现这里可能涉及到商业秘密,毕竟是签过保密协议的,所以在这部分我不会放出完整的代码,只会提供一个思路然后给出部分关键代码以供参考。这里我想根据我遇到的问题,以问题的方式来讲述这个项目,毕竟对于爬虫、框架这些东西大家都很熟再来讲这些也没有多大意思了用户登录在浏览器中操作的时候发现,如果是游客(也就是未登陆状态)的时候,当我们浏览相关用户的时间线时会得到下面这个界面在未登录的情况下查看用户信息的时候会弹出一个界面需要登录或者注册。因此从这里来看爬虫的第一个任务就应该是登录登录的时候scrapy提供了一个form_response的方法可以很方便的填写表单并提交,但是我发现用这种方式只能在返回的response对象中的request.headers里面找到cookie的字符串,而由于splash需要我们传入cookie的字典形式,这里我没有找到什么很好的办法,只能是采用splash 提供的方法。Facebook中登录页面为https://www.facebook/login。因此我重载爬虫的start_requests方法,提交一个针对这个登录页面url的请求。这个页面不涉及到渲染问题自然就使用Requests对象def start_requests(self): #开启爬取之前先登录 yield Request( url= self.login_url, # https://www.facebook.com/login callback= self.login, )当它请求的页面返回时触发login方法,在这个方法中我们提供了一个lua脚本自动填写用户名密码,然后提交请求,并最终返回成功的cookieyield SplashFormRequest.from_response( response, url = self.login_url, formdata={ "email":user, "pass": password }, endpoint="execute", args={ "wait": 30, "lua_source": lua_script, #这个参数是一个lua脚本的字符串 "user_name" : user, #user和password将会作为参数传入到lua脚本中 "user_passwd" : password, }, callback = self.after_login, errback = self.error_parse, )这里我们使用splash来发送请求包,这里我们主要向lua脚本中传入用户名和密码,下面是lua脚本的相关内容function main(splash, args) local ok, reason = splash:go(args.url) user_name = args.user_name user_passwd = args.user_passwd user_text = splash:select("#email") pass_text = splash:select("#pass") login_btn = splash:select("#loginbutton") if (user_text and pass_text and login_btn) then user_text:send_text(user_name) pass_text:send_text(user_passwd) login_btn:mouse_click({}) end splash:wait(math.random(5, 10)) return { url = splash:url(), cookies = splash:get_cookies(), headers = splash.args.headers, } end根据相关资料,SplashRequest 函数中的参数将会以lua table的形式被传入到splash形参中,而函数的args参数中的内容以 table的形式被传入到形参args中,所以这里要获取到用户名和密码只需要从args里面取即可上述lua代码首先请求对应的登录界面(我觉得这里应该不用请求,而直接使用response,但是这是我在写这篇文章的时候想到的还没有验证),然后通过css选择器找到填写用户名,密码的输入框和提交按钮。然后填写相关内容,最后点击按钮进行登录,然后等待一定时间,这里一定要等待以便Facebook服务器验证并跳转到对应的链接,最后我们是通过链接来判断是否登录成功。最后返回当前页面的url,cookie和对应的头信息在浏览器中执行登录操作的时候发现如果是新用户(没有填写相关信息用户)会跳转到www.facebook.com/?sk=welcome这个页面要求用户填入一定的信息,而老用户则会跳转到www.facebook.com 这个url,这个页面会显示用户关注的好友动态。因此在程序中我也根据跳转的新页面是否是这两个页面来进行判断是否登录成功的.登录成功后将脚本返回的cookie保存,脚本返回的信息在scrapy的response.data中作为字典的形式保存代理由于众所周知的原因,Facebook对于国内来说是一个404的站点,国内用户想要访问必须提供一个代理。在scrapy中代理可以设置在对应的下载中间件中,在下载中间件的process_request函数中设置request.meta["proxy"] = proxy但是这种方式针对splash时就不管用了,我找了很多资料发现可以在lua脚本中设置,每次在执行之前都需要相同的代码来设置代理,因此我们可以采用下面的模板function main(splash, args) splash:on_request(function(request) request:set_proxy{ host = '0.0.0.0', --代理服务器的IP port = 0, --代理服务器的端口 username = '', --登录的用户名和密码 password = '', type = "http", -- 代理的协议,根据官网的说法目前只支持http和ss5 ,目前就这个项目来说http就够了 } end) --do something end每次执行含有这段代码的脚本时首先执行on_request函数设置代理的相关信息,然后执行splash:go函数时就可以使用上面的配置访问对应站点了使爬虫保持登录状态根据splash的官方文档的说明,splash其实可以看做一个干净的浏览器,就好像我们在使用浏览器每次请求一个新页面的时候同时清理了里面的缓存一样,它不会保存之前的任何状态,所以这里的cookie只能每次在发包的同时给它设置上,好在splash给了相应的方法来设置和获取它,下面是关于cookie的模板local cookies = splash:get_cookies() -- ... do something ... splash:init_cookies(cookies) -- restore cookies至此我们的lua脚本的模板就变成了这样function main(splash, args) splash:init_cookies(splash.cookies) -- 这个cookie是通过SplashRequest函数的cookies参数传入的 splash:on_request(function(request) request:set_proxy{ host = '0.0.0.0', --代理服务器的IP port = 0, --代理服务器的端口 username = '', --登录的用户名和密码 password = '', type = "http", -- 代理的协议,根据官网的说法目前只支持http和ss5 ,目前就这个项目来说http就够了 } end) --do something return { cookie = splash:get_cookies() } end获取用户主页面我们在Facebook随便点击一个用户进入它的主页面,查看url如下可以看到针对用户名为英文的情况,它简单的将英文名作为二级目录,只不过将空格换成了点,而针对不为英文的用户,它以profile作为二级目录,并且后面带上一个参数id,这个ID就是用户的ID。其实根据后面我自己的实验不管上面的哪种用户都可以通过这个ID访问到,我们可以组成一个url:https://www.facebook.com/[id] 来访问用户首页,因此项目中要求提供的外部导入用户名必须是英文或者是ID,以便能直接通过url拼接的方式来获取用户首页除了这个区别之外,还有一种称之为公共主页的页面,比如下面是特朗普的公共主页对于公共主页来说它没有好友信息,没有时间线,因此针对这种页面的信息的解析可能需要别的方法。而光从url、id、和页面内容来看很难区分,而我在查找获取Facebook用户ID的相关内容的时候碰巧找到了它的区分方法,公共主页的HTML代码中只有一个page_id和profile_id,而个人的只有profile_id 其中用户ID就是这个profile_id比如下面分别是一个个人主页和公共主页搜索page_id的结果从上面的结果来看个人用户中page_id 只会出现在注释中,这是用浏览器请求的结果,其实在实际使用爬虫爬取到的结果中是搜不到这个id的,我们可以根据这个特性来区分,并且获取这两种主页的IDdef _get_user_info(self, html, url): key = "page_id=(\d+)" # 使用正则表达式获取page_id后面的id值 # page_id 只会出现在公共主页上,所以根据page_id来判断页面类型 pattern = re.compile(key) it = pattern.finditer(html) user_type = 0 try: it = next(it) #如果未找到这个地方会报StopIteration异常 user_type = TopUser.PUBLIC_PAGE # 公共主页 except StopIteration: # 未找到page_id 此时视为个人主页 key = "profile_id=(\d+)" #个人主页的id是profile_id的值 pattern = re.compile(key) it = pattern.finditer(html) try: it = next(it) user_type = TopUser.PRIVATE_PAGE except StopIteration: # 两个都没找到,此时视为页面错误,返回错误,爬虫停止 pass #TODO:解析对应的用户信息,这里主要解析用户id和页面类型获取时间线信息Facebook的用户时间线是通过异步加载的方式来进行的,我使用Chrome分析过它发送的异步请求,发现它里面是经过了加密的,因此不能通过解析它的响应包来获取相关信息,但是我们有splash这一大杀器,它就是一个浏览器,一般在加载更多信息的时候都会执行下来操作,所以说这里我们只要模拟这个下拉的操作就可以了,要操作这个浏览器当然是使用lua脚本了,下面是对应的lua脚本function main(splash, args) --前面是设置cookie和代理的操作 local ok, reason = splash:go(args.url) splash:wait(math.random(5, 10)) html = splash:html() old_html = "" flush_times = args.flush_times --这里是下拉次数,就好像操作浏览器一样每次下拉就会加载新的内容 i = 0 while(html ~= old_html) -- 当下拉得到的新页面与原来的相同,就认为它已经没有新的内容了,此时就返回 do old_html = html splash:runjs([[window.scrollTo(0, document.body.scrollHeight)]]) -- 执行js下拉页面 splash:wait(math.random(1,2)) -- 这里一定要等待,否则可能会来不及加载,根据我的实验只要大于1s就可以得到下拉加载的新内容,可能具体值需要根据不同的网络环境 if (flush_times ~= 0 and i == flush_times) then -- 当达到设置下拉上限并且不为0时推出,这里下拉次数为0表示一直下拉直到没有新内容 print("即将退出循环") break end html = splash:html() i = i + 1 end return { html = splash:html(), cookies = splash:get_cookies(), }上面的代码中,首先请求时间线的界面,然后获取相关的设置,主要是下拉次数。<br/>注意这里的下拉次数要根据超时值来设置,根据splash的官方文档,每个请求都有一个超时值,大于这个超时值会直接返回504 的错误这个时候就什么都得不到了,所以这里理想情况下是可以一直下拉的,但是由于有超时值的存在,必须给定一个下拉次数。我们可以在启动splash 的时候通过参数--wait-timeout给定。然后根据这个参数的设置一直下拉,直到没有更新或者达到最大下来次数。<br/>这个也有问题,如果网络不好或者其他情况导致没有加载出来,就认为它已经没有新内容了,这样会导致爬取内容不全这样我们就获取到了用户的时间线信息,具体的内容的解析就不再多说了,需要提醒一点的是,用户发帖中包含图片时分为三种情况,单个图片,多个图片(多个图片一般就被叫做albums——相册或者图集),简单的更新头像;这三种情况下页面的对应结构不同所以需要分情况,而且当时间线中包含视频的时候情况又不同获取公共主页的发帖信息公共主页中没有时间线,所以它的解析与个人主页的不同,好在Facebook提供了一种叫做图谱API的东西可以很方便的就可以获取到发帖信息。既然有这种API,为什么不用它获取个人用户信息呢?其实我也想用,就是要针对个人使用API就必须获取用户本人的确认,也就是要用户登录你的爬虫,然后授权给你,这自然是不可能的,所以针对个人用户只能简单的通过模拟浏览器的方式来解析HTML页面要使用Facebook的 API首先要获取一个access_token. 常规思路是先去去开发者平台注册一个开发者账号并建立一个应用。然后获取应用的token。但是我发现一般的应用Token 在获取公共主页的时候也存在一个授权的问题,好在Facebook提供了一个api的测试平台,而平台中提供了一个graph explore token,这个token可以不用授权,但是它只有一个小时的有效期,所以要使用API,首先就是从这个测试平台获取到这token。Facebook并没有提供任何有效方法来获取这个token,这个时候自然又要使用传统的方式,通过splash请求这个url,然后解析HTML获取对应token。针对这个问题,我处理的步骤如下:根据上一步获取到的页面类型,如果是公共页面,则先请求https://developers.facebook.com/tools/explorer/这个url,在登录状态下(前提是你的对应账号是Facebook的开发者账号),它会自动生成一个测试用的access_token就像下面这样输入框中就是token从该页面中获取到对应的token, 并调用对应的API获取公共主页的发帖信息,这里主要调用posts 并获取它的链接、ID、具体信息、图片、创建时间和编辑者 这些信息,具体的API文档参考Facebook官方文档,这里就不再介绍他们了def get_access_token(self, response): sel = Selector(response=response) access_token = sel.xpath('//label[@class="_2toh _36wp _55r1 _58ak"]/input[@class="_58al"]//@value').extract_first() #获取到token的值 #拼接API api = urljoin("https://graph.facebook.com/v3.0", response.meta["user_id"]) api = api + "/posts" + "?access_token=" + access_token + "&fields=link,id,message,full_picture,parent_id,created_time" yield Request( url=api, callback=self._get_public_posts, errback=self.error_parse )API返回的信息是以json格式返回的,下面是使用posts返回的一个例子,这里只是作为一个例子,请求返回的内容与项目中可能并不一样,但是并不影响针对它的分析{ "data": [ { "created_time": "2018-06-01T22:30:00+0000", "message": "Congratulations to our contest winners, Joseph and Dana! It’s always a pleasure to meet the people who make our MOVEMENT possible. Thank you!", "id": "153080620724_10161074525735725" }, ], "paging": { "cursors": { "before": "Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RBeU9UTTROekExTURBek9UVTBNakkwTnc4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TnpRMU1qVTNNelUzTWpVUEJIUnBiV1VHV3hISTZABRT0ZD", "after": "Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RFMk9UZA3pOemN4TkRrMU1EWXpNVFEwTlE4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TkRZANE1UazBNekEzTWpVUEJIUnBiV1VHV3dyV0FRRT0ZD" }, "next": "https://graph.facebook.com/v3.0/153080620724/posts?access_token=EAACEdEose0cBAMo5MUmbhGNfJlCGcrZArh7RBiCeSbwpqUq84tyEOs3HvO5KWoOtkWERRgkZBFjvOCb1DQ3go7PywUrA43SdJTqjeyjs5p2w3UanCFm0jxiE4Dt91A4qcGQYo9iobrLVrtCL0bdoNdkicQb6izFzxZBx0sPVauofQMLZCEFNLNcFqfkAinPWtSVeUQRxjQZDZD&pretty=0&limit=25&after=Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RFMk9UZA3pOemN4TkRrMU1EWXpNVFEwTlE4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TkRZANE1UazBNekEzTWpVUEJIUnBiV1VHV3dyV0FRRT0ZD" } }这个json主要分为两个部分一个是data,包含的是具体发帖的相关信息,另一个是paging,这个值里面包含了几个游标,其中next表示下一页的请求地址,我们只要判断出json中存在这个next就循环向这个next对应的url发包,当返回的json中不存在这个next时就标明已经到了最后一页。此时就解析了所有的发帖下面是具体的代码def _get_public_posts(self, response): rep_json = json.loads(response.text) data = rep_json["data"] post_user = response.meta["user"] for post in data: try: item = FbspiderItem() item["post_id"] = post["id"] item["post_user"] = post_user item["post_message"] = post["message"] item["post_time"] = post["created_time"] item["post_link"] = post["link"] yield item #存储图片的信息 item = FBPostImgItem() item["post_id"] = post["id"] item["img_url"] = post["full_picture"] yield item except KeyError: # 暂时不处理未找到帖子内容的情况 continue if "paging" not in rep_json: return paging = rep_json["paging"] if "next" in paging: api = paging["next"] yield Request( url = api, callback= self._get_public_posts, meta={"user" : post_user}, )获取好友信息获取好友的信息也需要采用模拟浏览器的方式,首先在用户页面上查找是否有好友的链接可以供点击,如果没有说明没有开放权限,当存在这个链接并点进去之后,可能并没有好友项,比如下面这样或者另外的情况,所以这里判断是否有好友信息需要两步,第一步是上面部分有好友这一栏,第二步是点进去之后在下面一栏中有全部好友这项内容。同样即使有好友,它也不会一次加载完毕,这里也用到下拉的相关操作。部分代码如下:function main(splash, args) -- 设置cookie和代理 local ok, reason = splash:go(args.url) splash:wait(math.random(5, 10)) friend_btn = splash:select("a[data-tab-key= 'friends']") --查找最上面那栏中是否有好友这个链接 if (friend_btn) then friend_btn:mouse_click({}) --点击进入好友页面 splash:wait(math.random(5, 10)) else return { hasFriend = false, cookie = splash:get_cookies(), } end return { hasFriend = true, html = splash:html(), cookie = splash:get_cookies(), url = splash:url(), } end执行完上述代码后,再分析是否有对应的好友信息,有的话就下拉刷新页面获取更多好友信息#当上面的代码执行完后进入这个函数 def _get_friends_page(self, response): hasFriend = response.data["hasFriend"] if not hasFriend: print("用户[%s]未开放好友查询权限" % response.meta["name"]) return html = response.data["html"] sel = Selector(response = response) friends = sel.xpath("//a[@name='全部好友']") if friends == []: print("用户[%s]未开放好友查询权限" % response.meta["name"]) return #获取代理,拼接对应的lua脚本 yield SplashRequest( url = response.data["url"], callback= self.parse_friends, endpoint="execute", cookies= random.choice(self.cookie), meta= {"level" : response.meta["level"], "name" : response.meta["name"]}, args={ "wait" : 30, "lua_source" : lua_script, } )当下拉结束之后就是解析页面获取页面中的好友信息了,在解析的时候发现,当点击某个好友进入它的主页面时,页面的链接为 https://www.facebook.com/profile.php?id=100011359746168&fref=pb&hc_location=friends_tab这个时候就会产生一种想法,这个id是不是就是用户id呢?我用这个id来直接访问用户主页行不行呢?经过我的实验,这个想法是对的,是可行的,因此对于好友这层用户我们可以直接拿到它的ID,不用向前面一样根据名称来拼接,下面是解析好友信息的部分代码def parse_friends(self, response): sel = Selector(response = response) friends = sel.xpath("//li[@class='_698']//div[@class='fsl fwb fcb']//a") #拼接lua脚本 for friend in friends: url = friend.xpath(".//@href").extract_first() name = friend.xpath(".//text()").extract_first() #记录当前好友关系 friend_item = FBUserItem() friend_item["user_name"] = response.meta["name"] friend_item["friend_name"] = name print("提取到好友信息%s : %s" % (friend_item["user_name"], friend_item["friend_name"])) yield friend_item #这里再次提交请求主要是为了获取好友的好友,以及获取好友发的帖子 #其实也可以在这个请求执行完成之后解析用户主页面得到用户的ID等信息 yield SplashRequest( url = url, endpoint="execute", callback= self.parse_main_page, meta={"name" : name, "level" : level}, cookies = random.choice(self.cookie), # 从cookie池中随机取出一个cookie args={ "wait": 30, "lua_source": lua_script, } )反爬虫的相关操作针对爬虫程序来说最头疼的就是有的站点在反爬虫这块做的太好了,Facebook就是这样的一个站点,我的测试账号在执行程序的时候被封过无数次。为了防止被封我主要采取了这样几个措施减少并发数,设置发包的延时这些内容主要在scrapy的配置文件中控制DOWNLOAD_DELAY = 5 # 发包延时 CONCURRENT_REQUESTS = 16 # 最大并发数设置代理池代理池的设置通过下载中间件的process_request函数来设置,设置的相关代码如下:def process_request(self, request, spider): if self.proxies != []: proxy = random.choice(self.proxies) # self.proxies 是一个含有多个代理的列表,从中随机取一个 print("启用代理:%s" % proxy) if "splash" in request.meta: #判断是否是一个splash请求 request.meta['splash']['args']['proxy'] = proxy# 设置splash代理 else: request.meta["proxy"] = proxy #设置scrapy的代理这里虽然判断了是否为一个splash,但是针对splash的请求,这种设置方式无效,需要采用之前介绍的方式在LUA脚本中设置,而随机设置的方法就是在一组代理中随机选择一个传入对应的LUA脚本中设置UA在下载中间件的process_request函数中来设置,设置的方法与设置代理的方法类似class RotateUserAgentMiddleware(UserAgentMiddleware): def __init__(self, user_agent=''): self.user_agent = user_agent def process_request(self, request, spider): ua = random.choice(self.user_agent_list) if ua: print("启用UA :%s" % ua) request.headers.setdefault('User-Agent', ua) user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1" "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ] 设置多用户登录这里我设置了多个登录用户,通过从用户的登录cookie池中随机选取一个作为请求的cookie,在爬虫开始位置导入多个用户的用户名和密码信息,依次登录,登录成功后保存用户cookie到列表,后面每次发包前随机选取一个cookie设置SplashReuqests函数的等待时间就像前面代码中每个SplashRequest函数的args参数中总会带有 一个wait的键值,这个表示每次接到请求后等待的时长,加上这个是为了减慢爬虫运行速度防止由于发包过快导致账号被封至此,我已将之前涉及到的所有问题基本上都提到了,很多地方我自认为处理的不是很完美,但是写出来的内容勉强够用。这个爬虫项目我最大的收获就是知道了splash这个好用的东西,可惜的是它并没有中文的文档,所以像我这样刚过四级的人读起来还是有点吃力的。所以为了方便他人学习,以及提高我的英文水平我决定乘着这段时间我有空闲我想翻译它的官方文档。目前项目刚刚开始,地址为:splash中文文档PS: 不知道这个项目取这个名字会不会涉及到虚假宣传或者版权什么的,如果涉及到这些我会立马改名最后的最后列举出项目中参考的文档,在整个项目中参考的文档实在太多,不会一一列举,这里只列举我印象最深的一些回归爬虫,拥抱scrapy&splash。抓facebook public post like、comment、shareSplash官方文档Scrapy文档scrapy_splash项目文档最后再说两句没想到这个博客有这么多人来看并且评论,最近很长时间没有登录CSDN的博客,今天再登录上来才发现有许多新的留言,这些留言我没有及时回复,也不知道那些有问题的朋友,你们的问题解决了没有。在此我对自己的失误表示抱歉。另外我想额外说几点:之前有朋友通过qq联系到我,咨询相关问题,我基本上把我知道的都说出来了,我在这篇博客后面也贴出了splash相关的文档,有英文也有我自己翻译的中文的,各位朋友可以自己根据自己的情况结合着来看。在提问之前请仔细阅读相关文档,我想很多在文档上有的内容就不要来问我了。毕竟我翻译这个文档已经有些日子了,很多细节可能我自己都不太记得了。如果文档哪些地方翻译的不太清楚的,欢迎大家批评指正。另外有很多朋友私信我,问我想不想接一些私活什么的,在这里感谢这些朋友的信任。在这里我想说一下,暂时我没有接这方面的私活的准备。第一:我自己最近比较忙,在我的新年计划中提到,我目前正在进行角色转换,不仅有自己的事,也要盯着手下的人完成任务,加班比较多,没有什么时间来做私活。第二,不同的网站差距还是很大的,从用户权限验证到反爬虫机制都不一样,而且不同的私活需求也不一样,需要花大量的时间来分析目标网站。我自己并不想以后专门钻研爬虫,所以不太想花太多的时间在这种项目上。第三,我也有自己的职业规划,我希望自己能在多余时间内提升自己的职业技能。第四我自己其实没有多少信心来完成新接的项目,因为这个项目是延期的,大概延期了一周左右,而且很多地方没有我当初设想的那么简单,而且多多少少有一些问题最后无法解决,但是它并不是一个纯商业项目,只是一个简单的玩具而已。所以说实话我可能并没有独立开发一整套商用爬虫的能力。第五,大家都是在网上的陌生人,我也怕被骗,而且爬虫从某种程度上可能有法律上的风险。基于以上几点我想暂时不接受任何形式的私活,最后谢谢各位的信任
2018年06月02日
6 阅读
0 评论
0 点赞
2018-05-27
ATL模板库中的OLEDB与ADO
上次将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;这次就不再放上整体例子的链接了,有之前的基础应该很容易看懂这些,而且这次代码比较短,基本上将所有代码全粘贴了过来。
2018年05月27日
6 阅读
0 评论
0 点赞
2018-05-19
OLEDB事务
学过数据的人一般都知道事务的重要性,事务是一种对数据源的一系列更新进行分组或者批处理以便当所有更新都成功时同时提交更新,或者任意一个更新失败时进行回滚将数据库中的数据回滚到执行批处理中的所有操作之前的一种方法。使用事务保证了数据的完整性。这里不展开详细的说事务,只是谈谈OLEDB在事务上的支持ITransactionLocal接口OLEDB中支持事务的接口是ITransactionLocal接口,该接口是一个可选接口,OLEDB并不强制要求所有数据库都支持该接口,所以在使用之前需要先判断是否支持,好在现在常见的几种数据库都支持。该接口属于回话对象,因此要得到该接口只需要根据一个回话对象调用QueryInterface即可调用接口的StartTransaction方法开始一个事务该函数的原型如下HRESULT StartTransaction ( ISOLEVEL isoLevel, ULONG isoFlags, ITransactionOptions *pOtherOptions, ULONG *pulTransactionLevel); 第一个参数是事务并发的隔离级别,一般最常用的是ISOLATIONLEVEL_CURSORSTABILITY,表示只有最终提交之后才能查询对应数据库表的数据第二个参数是一个标志,目前它的值必须为0第3个参数是一个指针,它可以为空,或者是调用ITransactionLocal::GetOptionsObject函数返回的一个指针第4个参数是调用该函数创建一个事务后,该事务的并发隔离级别隔离级别是针对不同的线程或者进程的,比如有多个客户端同时在操作数据库时,如果我们设置为ISOLATIONLEVEL_CURSORSTABILITY,那么在同一事务中只有当其中一个客户端提交了事务更新后,另外一个客户端才能正常的进行查询等操作,可以简单的将这个标识视为它在数据库中上了锁,只有当它完成事务后其他客户端才可以正常使用数据库开始一个事务后正常的进行相关的数据库操作当所有步骤都正常完成后调用ITransaction::Commit方法提交事务所做的所有修改或者当其中有一步或者几步失败时调用ITransaction::Abort方法回滚所有的操作演示例子cppcpp//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); //获取主表主键的最大值//获取主表主键的最大值 pRetData = pRetData = RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary"));RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary")); if(NULLif(NULL == pRetData)== pRetData) {{ goto CLEAR_UP;goto CLEAR_UP; }} iPID = iPID = *(int*)((BYTE*)pRetData +*(int*)((BYTE*)pRetData + sizeof(DBSTATUS)sizeof(DBSTATUS) ++ sizeof(ULONG));sizeof(ULONG)); //最大值总是加1,这样即使取得的是空值,起始值也是正常的1//最大值总是加1,这样即使取得的是空值,起始值也是正常的1 ++iPID;++iPID; TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszPrimaryTable;(LPOLESTR)pszPrimaryTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_PID//设置第一个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = iPID;= iPID; //设置第二个字段 F_MValue//设置第二个字段 F_MValue *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) ,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据"));,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据")); //插入新数据//插入新数据 hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); COM_SAFEFREE(pChangeBindings);COM_SAFEFREE(pChangeBindings); COM_SAFEFREE(pRetData);COM_SAFEFREE(pRetData); COM_SAFEFREE(pbNewData);COM_SAFEFREE(pbNewData); if(NULLif(NULL != hChangeAccessor &&!= hChangeAccessor && NULLNULL != pIAccessor)!= pIAccessor) {{ pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); hChangeAccessor = hChangeAccessor = NULL;NULL; }} COM_SAFERELEASE(pIAccessor);COM_SAFERELEASE(pIAccessor); COM_SAFERELEASE(pIRowsetChange);COM_SAFERELEASE(pIRowsetChange); COM_SAFERELEASE(pIRowsetUpdate);COM_SAFERELEASE(pIRowsetUpdate); //插入第二个也就是从表的数据//插入第二个也就是从表的数据 TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszMinorTable;(LPOLESTR)pszMinorTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_MID//设置第一个字段 K_MID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); //设置第二个字段 K_PID//设置第二个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) = iPID;= iPID; //设置第二个字段//设置第二个字段 *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[2].obValue)(pbNewData + pChangeBindings[2].obValue) ,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据"));,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据")); for(int i = iMIDS; i <= iMIDMax; i++)for(int i = iMIDS; i <= iMIDMax; i++) {//循环插入新数据{//循环插入新数据 //设置第一个字段 K_MID//设置第一个字段 K_MID *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = i;= i; hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); }} hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); //所有操作都成功了,提交事务释放资源//所有操作都成功了,提交事务释放资源 hr = pITransaction->Commit(FALSE, XACTTC_SYNC, hr = pITransaction->Commit(FALSE, XACTTC_SYNC, 0);0); COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr); CLEAR_UP:CLEAR_UP://操作失败,回滚事务先,然后释放资源//操作失败,回滚事务先,然后释放资源 hr = pITransaction->Abort(NULL, FALSE, FALSE); hr = pITransaction->Abort(NULL, FALSE, FALSE);在上述代码中首先创建一个事务对象,然后在进行相关的数据库操作,这里主要是在更新和插入新数据,当所有操作成功后调用commit函数提交,当其中有错误时会跳转到CLEAR_UP标签下,调用Abort进行回滚 最后实例的完整代码: [Trancation](https://gitee.com/masimaro/codes/tcesnrul0g2yi76bam5dj19#Trancation) <!-- more -->
2018年05月19日
1 阅读
0 评论
0 点赞
1
...
19
20
21
...
32