Masimaro's blog - winsock2
type: channel
link: https://www.masimaro.top/tag/winsock2/
items: WinSock Socket 池
items: WinSock2 API
items: WinSock 重叠IO模型
items: WinSock WSAEventSelect 模型
items: WSAAsyncSelect 消息模型

WinSock Socket 池
type: item
date: 2018-08-12T16:23:00+08:00
description: 之前在WinSock2.0 API 中说到，像DisConnectEx 函数这样，它具有回收SOCKET的功能，而像AcceptEx这样的函数，它不会自己在内部创建新的SOCKET，需要外部传入SOCKET作为传输数据用的SOCEKT，使用这两个函数，我们可以做到，事先创建大量的SOCKET，然后使用AcceptEx函数从创建的SOCKET中选择一个作为连接用的SOCKET，在不用这个SOCKET的时候使用DisConnectEx回收。这样的功能就是一个SOCKET池的功能。SOCKET池WinSock 函数就是为了提升程序的性能而产生的，这些函数主要使用与TCP协议，我们可以在程序启动的时候创建大量的SOCKET句柄，在必要的时候直接使用AcceptEx这样的函数来使用已有的SOCKET作为与客户端的连接，在适当的时候使用WSARecv、WSASend等函数金星秀数据收发操作。而在不用SOCKET的时候使用DisConnectEx 回收，这样在响应用户请求的时候就省去了大量SOCKET创建和销毁的时间，从而提高的响应速度。IOCP本身也是一个线程池，如果用它结合WinSock 的线程池将会是Windows系统上最佳的性能组合，当然在此基础上可以考虑加入线程池、内存池的相关技术来进一步提高程序的性能。这里我想顺便扯点关于程序优化的理解。程序优化主要考虑对函数进行优化，毕竟在C/C++中函数是最常用，最基本的语法块，它的使用十分常见。函数的优化一般有下面几个需要考虑的部分是否需要大量调用这个函数。针对需要大量调用某个函数的情况，可以考虑对算法进行优化，减少函数的调用函数中是否有耗时的操作，如果有可以考虑使用异步的方式，或者将函数中的任务放到另外的线程（只有当我们确实不关心这个耗时操作的结果的时候，也就是说不涉及到同步的时候）函数中是否有大量的资源调用，如果有，可以考虑使用资源池的方式避免大量资源的申请与释放操作下面是一个使用SOCKET池的客户端的实例#include <stdio.h> #include <process.h> #include "MSScokFunc.h" #define SERVICE_IP "119.75.213.61" #define MAX_CONNECT_SOCKET 500 unsigned int WINAPI IOCPThreadProc(LPVOID lpParam); LONG g_nPorts = 0; CMSScokFunc g_MsSockFunc; typedef struct _tag_CLEINT_OVERLAPPED { OVERLAPPED overlapped; ULONG ulNetworkEvents; DWORD dwFlags; DWORD wConnectPort; DWORD dwTransBytes; char *pBuf; DWORD dwBufSize; SOCKET sConnect; }CLIENT_OVERLAPPED, *LPCLIENT_OVERLAPPED; SOCKADDR_IN g_LocalSockAddr = {AF_INET}; int main() { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SYSTEM_INFO si = {0}; const int on = 1; GetSystemInfo(&si); HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, si.dwNumberOfProcessors); HANDLE *pThreadArray = (HANDLE *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(HANDLE) * 2 * si.dwNumberOfProcessors); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, IOCPThreadProc, &hIOCP, 0, NULL); pThreadArray[i] = hThread; } g_MsSockFunc.LoadAllFunc(AF_INET, SOCK_STREAM, IPPROTO_IP); LPCLIENT_OVERLAPPED *pOverlappedArray = (LPCLIENT_OVERLAPPED *)HeapAlloc(GetProcessHeap(…

WinSock2 API
type: item
date: 2018-07-21T16:44:00+08:00
description: WinSock中提供的5种网络模型已经可以做到很高效了，特别是完成端口，它的高效的原因在于它不仅另外开启了线程来处理完成通知而不是占用主程序的时间，同时也在于我们在完成端口中运用了大量异步IO处理函数。比如WSASend、WSARecv等等。为了高效的处理网络IO，WinSock提供了大量这样的异步函数。这篇博文主要探讨这些函数的用法和他们与传统的巴克利套接字相比更加高效的秘密AcceptEx其实在使用TCP协议编程时，接受连接的过程也是需要进行收发包操作的，具体的过程请参考TCP的三次握手。针对这种特性WinSock提供了对应的异步操作函数AcceptEx。函数原型如下:BOOL AcceptEx( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );sListenSocket: 监听套接字sAcceptSocket：该参数是一个SOCKET的句柄，一旦连接成功建立，那么会使用该SOCKET作为通信的SOCKETlpOutputBuffer：是三个数据一体化的缓冲区的指针，这三个数据分别是接收连接时顺带接收客户端发过来的数据的缓冲，之后是本地地址结构的缓冲，最后是远程客户端地址结构的指针dwReceiveDataLength：是lpOutputBuffer的缓冲长度dwLocalAddressLength：是本地地址结构长度,其值等于sizeof(SOCKADDR)+16dwRemoteAddressLength：是远程客户端地址结构长度,其值也等于sizeof(SOCKADDR)+16lpdwBytesReceived：该参数用于返回接受连接请求时接收的数据的长度lpOverlapped：就是重叠I/O需要的结构第一个参数是一个十分重要的参数，这个参数是AcceptEx比较高效的一个重要的原因。从功能上来看它与传统的accept函数并没有什么区别，都是接受客户端连接的。它与accpet相比比较高效的原因如下：从内部机理来说accpet在内部其实有一个创建SOCKET的操作，当函数成功后会返回这个SOCKET，所以AcceptEx与accept相比少了一个创建SOCKET的操作，它的功能更加纯粹，这就给了我们一个启示：我们可以在初始化的时候创建大量的SOCKET，并投递到AcceptEx中，这样在接受连接时省去了创建SOCKET的时间，能够更快速的响应客户端的连接。由于AcceptEx不用创建SOCKET，所以它也将accept内部对socket设置的操作给省去了，也少了一些其他的附带操作，比如地址的解析，其实这里我们可以简单的理解为lpOutputBuffer中保存的信息就是TCP三次握手中的SYN包和ACK包，这些包的信息需要在函数返回后由用户通过其他方法来解析，而accpet帮我们解析了，所以AcceptEx比accept更加高效因为AcceptEx的设计目标纯粹就是为了性能,所以监听套接字的属性不会被代表客户端通讯的套接字自动继承。要继承属性(包括socket内部接受/发送缓存大小等等)就必须调用setsockopt使用选项SO_UPDATE_ACCEPT_CONTEXT,如下:int nRet = ::setsockopt(skClient, SOL_SOCKET ,SO_UPDATE_ACCEPT_CONTEXT ,(char *)&skListen, sizeof(skListen));AcceptEx函数除了能够接受客户端的连接之外，它也可以在接受连接的同时接收客户端随着连接请求一块发过来的数据，只要我们设置dwReceiveDataLength 参数大于0，并在lpOutputBuffer中分配相应的缓冲即可，但是这里会存在一个安全问题，当我们设置了这些之后，如果客户端只发送连接请求，但是不发送数据，AcceptEx会一直等待，如果有大量这样的客户端，那么可能会给服务器造成大量的资源浪费从而不能及时的服务其他正常客户端。要防止这样的情况，可以采用下列措施：设置dwReceiveDataLength为0，并且不分配对应的缓冲，也就是关闭这个接收数据的功能。启动一个监视线程对用于连接的SOCKET轮询调用：int iSecs; int iBytes = sizeof( int ); getsockopt( hAcceptSocket, SOL_SOCKET, …

WinSock 重叠IO模型
type: item
date: 2018-06-29T17:47:00+08:00
description: 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方式的SO…

WinSock WSAEventSelect 模型
type: item
date: 2018-06-23T18:13:00+08:00
description: 在前面我们说了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_REA…

WSAAsyncSelect 消息模型
type: item
date: 2018-06-03T09:42:00+08:00
description: 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); Message…