服务器IO模型之Select

13 篇文章 0 订阅
5 篇文章 0 订阅

阻塞与非阻塞:

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值