服务器IO模型之Select

来源:互联网 发布:js new image 编辑:程序博客网 时间:2024/06/06 19:51

阻塞与非阻塞:

widows下创建套接字默认都是阻塞型的,阻塞型的好处是处理简单,理解容易,但是处理多个套接字时,就必须创建多个线程,即一个连接socket使用一个线程。而非阻塞模式比如在处理发送和接收数据时,会立即返回,不管是否有有效的数据,这就需要不断测试返回代码,来确定套接字在什么时候可读/可写,也就是确定网络事件何时发生,比如中断默认就是一种事件触发型,比如菜单按钮也是事件触发性,但是比如快递邮寄包裹,他其实使用的是一种任务制(提前规定好的)。windows也提供了众多的非阻塞I/O模型,如select、WSAAsyncSelect、WSAEventSelect、overlapped、completion port,比如select就可以设置时间,按规定时间去查询事件是否被触发,像WSAAsyncSelect就是事件驱动型的,等,这里主要用在socket端开发服务器程序。

select模型目的:主要是避免在套接字调用上阻塞的应用程序有能力管理多个套接字,即是单一线程模式下只能处理一个套接字的问题,这样可以避免线程膨胀。

select模型函数:

int select(  _In_     int nfds,  _Inout_  fd_set *readfds,  _Inout_  fd_set *writefds,  _Inout_  fd_set *exceptfds,  _In_     const struct timeval *timeout);
参数说明:

nfds [in]:忽略,仅是为了兼容Berkeley套接字

readfds [in, out]:用来检查可读的套接字组合

writefds [in, out]:用来检查可写的套接字组合

exceptfds [in, out]:用来检查异常的套接字组合

timeout [in]:等待的时间, 如果为NULL,等待的时间为无穷大

返回值:select返回那些即将要被处理的socket总和,假如时间超时,将会返回SOCKET_ERROR,可以使用WSAGetLastError获得出错的原因

Select处理过程:假设以read为例,在这里windows主要是先将套接字s添加到readfds集合中,然后等待select函数返回,在select函数里面会移除没有未决的I/O操作的套接字句柄,即移除未响应的IO套接字句柄,然后看s是否认仍然还是readfs集合中,在就说明s可读了

应用程序:

 CInitSock initsock;sockaddr_in addr;USHORT usPort = 6000;SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP );if (sListen == INVALID_SOCKET ){TRACE("create socket error:%d\n",  WSAGetLastError());return -1;}addr.sin_family = AF_INET;addr.sin_port = htons(usPort);addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);if (::bind(sListen, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR){/*WSAEFAULT 10014:The system detected an invalid pointer address   in attempting to use a pointer argument in a call.*/TRACE("bind socket error: %d\n",  WSAGetLastError()); return -1;}::listen(sListen, 5);fd_set fdSocket;FD_ZERO(&fdSocket);FD_SET(sListen, &fdSocket);while (true){fd_set fdRead = fdSocket;int nRet = select(0, &fdRead, NULL, NULL, NULL);if (!nRet || nRet == SOCKET_ERROR  ){TRACE("select error: %d\n",  WSAGetLastError()); return -1;}for (unsigned int i = 0; i < fdSocket.fd_count; i++){if (FD_ISSET(fdSocket.fd_array[i], &fdRead)) //这里选择了fdSocket,是因为下一次循环还要使用fdSocket{if (fdSocket.fd_array[i] == sListen){if (fdSocket.fd_count < FD_SETSIZE){sockaddr_in addrRemote;int nAddrLen = sizeof(addrRemote);SOCKET sNewClient = ::accept(sListen, (sockaddr*)&addrRemote, &nAddrLen);if (sNewClient == INVALID_SOCKET ){TRACE("accept new client socket error: %d\n",  WSAGetLastError());break;}FD_SET(sNewClient, &fdSocket);TRACE("new client: %s\n", inet_ntoa(addrRemote.sin_addr));}}else{char szText[256];int nRecv = ::recv(fdSocket.fd_array[i], szText, sizeof(szText), 0);if (!nRecv || nRecv == SOCKET_ERROR ){TRACE("recv data error: %d\n", WSAGetLastError());closesocket(fdSocket.fd_array[i]);FD_CLR(fdSocket.fd_array[i], &fdSocket);break;}else{szText[nRecv] = '\0';TRACE("recv data: %s", szText);}}}//判断fdsocket里面的socket是否得到处理}}closesocket(sListen);sListen = INVALID_SOCKET;return 0;
这里我使用了CInitSock, 因为在使用socket之前要加载Ws2_32.lib,这里我定义了一个类如下:

#pragma once#include<winsock2.h>#include <ws2tcpip.h>#pragma comment(lib,"Ws2_32.lib")class CInitSock{public:CInitSock(BYTE minVer = 2, BYTE majVer = 2);~CInitSock(void);};
CInitSock::CInitSock(BYTE minVer, BYTE majVer){int nResult;WSADATA wsadata;WORD wVerReq = MAKEWORD(minVer, majVer);if (nResult = ::WSAStartup(wVerReq, &wsadata)){TRACE("WSAStartup Load DLL Failed: %d!\n", nResult);}}CInitSock::~CInitSock(void){/*In a multithreaded environment, WSACleanup terminates  *Windows Sockets operations for all threads. */::WSACleanup();}
默认构造函数里面有一个默认加载的版本,这里在析构函数里面将之前加载的dll资源进行释放,基础的socket服务器模型通常要进行socket创建,绑定到本地地址和端口,监听客户端的连接,一旦有客户端连接,默认会放入fdSocket中,然后将此函数加入fd_set可读的套接字集合中,select返回后,未响应的socket会被移除,即将要被处理的socket会保留下来,然后从fdSocket判断,到底是哪些socket发生了可读操作:

注意:可读操作包括有未处理的连接请求,数据可读,连接关闭/重启/中断

首先第一个判断的就是未处理的连接请求,如果有就建立新的连接通道,加入fdSocket;如果是数据可读,就读取数据;连接关闭会在下面进行测试
客户端程序:使用的是以前的一个简易客户端程序,如下

WORD wVersionRequested; //请求的版本WSADATA wsaData;int nErr;//协商版本号wVersionRequested = MAKEWORD(1,1);nErr = WSAStartup(wVersionRequested, &wsaData);if(nErr != 0){return;}if( LOBYTE(wsaData.wVersion) != 1 ||HIBYTE(wsaData.wVersion) != 1 ){WSACleanup();return;}//创建socket端口SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr= inet_addr("127.0.0.1");addrSrv.sin_family= AF_INET;addrSrv.sin_port= htons(6000);//绑定端口号connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//收发数据char recvBuf[100], sendBuf[100];memset(recvBuf, 0, 100);memset(sendBuf, 0, 100);sprintf_s(sendBuf,"hello world");send(sockClient, sendBuf, strlen(sendBuf)+1, 0);recv(sockClient, recvBuf, 100, 0);printf("%s\n", recvBuf);//关闭socket通信closesocket(sockClient);WSACleanup();Sleep(1000);
测试结果:

这里需要先运行服务器端,然后再开启客户端程序,服务器端会建立新的连接,并读取客户端发过来的数据然后显示出来,客户端是没有数据的,因为这里服务器端并没有发送数据,如下服务器数据:

再次开启一个客户端的效果如下:

这里并没有进行换行,两行数据在一起了,并不影响测试结果,这里还可以再强制关闭客户端后的结果,如下

这里看到error的代码为10054,我们在winerror.h里面找到如下定义:

//// MessageId: WSAECONNRESET//// MessageText://// An existing connection was forcibly closed by the remote host.//#define WSAECONNRESET                    10054L
从这里也能看出的确是强制关闭,注意服务器端里面的TRACE要给我printf才可以,TRACE默认是在调试下使用的输出语句

Select不足:其实添加到fd_set套接字数量是有限制的,winsock2.h定义的64,自定义也不超过1024,因为值太大,会对服务器的性能有影响,假设有1000个的话,在调用select之前就必须设置这1000个套接字,select返回之后,还必须检查这1000个套接字,所以开销较大。

0 0