winsock API中的两个函数recv()和send()都是阻塞式的,这种通信效率是很低的。
举这么一个例子吧,从前有一个妓女和一个嫖客,这个嫖客呢每天都要逛窑子和那个妓女OOXX。所以那个妓女每天就在门口等待嫖客的到来,若嫖客没来,她也一直傻等在那里。即便有其他嫖客想来,妓女也不带理的。
这就是阻塞式的方式,阻塞式的意思就是函数若没有执行成功,则不返回。这是大多数函数的做法,但是对于通信中的recv和send来说,这种方法是十分低效的,因为这两个函数会使得整个线程都阻塞住,无法做其他事情。就好比那个傻等嫖客的妓女一样。
那么为什么通信程序要使用I/O模型?I/O模型就是应用程序提供一个缓冲区,在缓冲区进行收发数据。如果函数recv和send没有执行成功,则函数立即返回,线程可以先做其他的事情。等下次有数据要收或要发时,再执行函数。这样的函数就是非阻塞式的。
也就是说,当那个妓女看到嫖客没有来时,她可以采取一些行动,来增加她的接客效率。这,就是I/O模型。微软为了提高妓女的接客效率,一共提供了5种I/O模型。
这次说说最简单的select模型。select模型的核心就是select函数,它的原型是:
int select(int nfds,
fd_set FAR* readfds,
fd_set FAR* writefds,
fd_set FAR* exceptfds,
const struct timeval FAR* timeout);
第一个函数可忽略,置0即可。第二个是设读集(其实就是读取缓冲区中的数据),对应recv()函数用。第三个设写集,对应send()函数用。第四个设带外数据,一般用于查看错误。第五个是设置等待时间,这个参数比较重要。下面是一个封装了select模型的函数:
int ClientStartRecv(char *recvBuffer,int bufLenth)
{
fd_set recvset;
TIMEVAL timeout;
timeout.tv_sec=0;
timeout.tv_usec=0;
int recvByte=0;
FD_ZERO(&recvset);
FD_SET(appClientSocket,&recvset);
int ret=select(0,&recvset,NULL,NULL,&timeout);
if(ret>0 && FD_ISSET(appClientSocket,&recvset))
{
recvByte=recv(appClientSocket,recvBuffer,bufLenth,0);
}
return recvByte;
}
从这里也可以看出,select模型还是比较容易使用的。这里重点说说timeout这个参数,如果这个参数设置成(0,0),那么recv函数在发现没有数据可读时,就立即返回。如果设置成一个大于0的值,则recv函数等待相应的时间后返回。如果timeout是一个空指针,则函数变成阻塞式,直到至少一个描述符正好碰到指定的条件为止。那么这个参数具体要设置成多大的数值最好呢。
还是以刚才的例子来说明:妓女在发现嫖客没来的时候,立刻回房,过了一会儿,又去门口看看嫖客来了没有,如此反复。将参数设置成(0,0)就会这样。
如果设置一个等待时间,那么妓女就会在门口等待一段时间,如果嫖客没来则回房。过了一会儿,又去门口看看嫖客来了没有,如此反复。将timeout设为大于零的值就是这样。
如果是空指针,妓女则在门口一直等待,然后看到任何一个看似嫖客样子的人,就将其拉去OOXX。这个就是将timeout设成空指针的形式。这是很危险的,因为你怎么知道这个看似嫖客的人有没有病或者不是强盗、或者没钱的人。很显然,这个随便拉来的客人并不是妓女本身想要的。
说了那么多,其实我也不知道timeout该设多少。我的前辈跟我说timeout设(0,0),而《windows网络编程技术》这本书则说应该尽量避免这样设置,因为会这样的设置会让select变成轮询操作,使其效率变低。实际上,MSDN并没有这么说,所以我都是设成(0,0)。
这里开源一个前些天完成的一个通信lib,用的就是select模型写的。我的编译环境是VS2005,更低的版本VC只要拷贝.h和.cpp,然后自己新建一个库项目,将两个文件放入工程中,重新编译一下就可以了。
我曾经对别人写好的lib和class很依赖,现在想来还是自己写的东西最好,即便有BUG,调试起来也知道该怎么入手。反观用现成的lib,如果它是成熟的,那么可能会增加开发效率。如果是一些不成熟甚至本身有bug的库,那么只能增加调试程序的负担。所以用别人的库写程序前,还是要先想想清楚先的。至少这个库得是开源的,可以清楚的看到它的代码实现。
另外,以后我要继续用妓女和嫖客的例子来说明其他4种I/O模型。我觉得这便于自己的理解,哈哈。
