写网络程序服务器代码时,会遇到同时与多个client进行通信的问题,假如这些与client连接的socket都存储在SockArray[max_num]中,为了接受每个client的数据,可能会这么做:
这样做会遇到一些问题:
1.recv函数默认是阻塞式的,阻塞是什么意思呢,就是这个函数非要执行完才会返回,例如recv非要等到client有数据才返回,若此时另外的client有数据发给server就悲剧了。
对此的解决办法有,采用非阻塞式的,recv不必要等到有数据,并接收成功才返回,只是测试一下有没有数据来了,没有直接返回,利用函数的返回值来了解函数真正的运行情况。
由于linux下一切皆文件。所以不管open还是socket的返回值都是文件描述符,都是阻塞的,可以调用fcntl函数使其变成非阻塞的。
fcntl函数改变文件描述符的属性,函数的几种形式:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
cmd的几种形式:
要设置文件为非阻塞,要先用cmd = F_GETFD 获得其文件属性,然后设置非阻塞属性
可以写一个设置或移除文件的某个属性的函数:
如果要某个socket返回的文件描述符设置为非阻塞,只需要:
SetOrRemoveFlag(fd,true,O_NONBLOCK);
采用非阻塞IO机制来实现的代码,与原来的相比,结构和原理上没有改变,只是把所有的IO都设置为非阻塞的。根据I/O函数返回值>0来确定有数据进行I/O。
2.这时候的代码采用非阻塞式依然有一个问题,那就是for循环不停的在跑,来检查是否有socket可以进行I/O,浪费了大量的CPU时间。
3.如何解决呢?其实可以采用阻塞加轮询的方式,某个函数去检查是否有socket可以I/O,如果没有它就阻塞,过一定时间返回,如果有socket可以I/O,就进行I/O。
这就是select函数的功能:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
各个参数含义:
fd_set,是一个文件描述符的集合。
如此,readfds则是一个读文件描述符集合,也就这个集合中的文件想检查其是否可以进行读操作。
同理,writefds中的文件,都想关注其是否可以进行写操作。
exceptfds,关心一个描述符的异常情况。
struct timeval的定义:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
*如果timeout = NULL,则这个函数变成阻塞式的,即永远等待
*如果timeout = 0,则这个函数变成非阻塞式的,即完全不等
*如果非以上两种情况,select函数在检查各个集合中的描述符若都没有准备好,则等待timeout的时间后,返回。
从select返回时,内核告诉我们:
1)已准备好的描述符数量
2)哪一个描述符已准备好读、写或异常条件
然后就可以进行相应的I/O操作。
select函数返回值:
*-1,函数出错
*0,等待超时
*n,已准备好的描述符数量
seletct函数第一个参数含义:
所有描述符集合中描述符最大值+1,内核要遍历检索这些描述符是否准备好相应操作。
关于结构体fd_set:
每个描述符对应于数据结构fd_set所占用内存空间的一个位,如果第i位为0则表示值为i的描述符不包含在该集中,反之亦然。
为了方便用户使用,系统提供了如下的四个宏进行操作。
FD_ZERO(fd_set *fdset); //清空fdset中的所有位
FD_SET(int fd, fd_set *fdset); //在fdset中打开fd所对应的位
FD_CLR(int fd, fd_set *fdset); //在fdset中关闭fd所对应的位
FD_ISSET(int fd, fd_set *fdset); //测试fd是否在fdset中
所以一般的操作如下:
fd_set rset;
int fd;
FD_ZERO(&rset); //必须使用FD_ZERO清除其所有位
FD_SET(fd, &rset); //然后设置我们所关心的位
if( FD_ISSET(fd, &rset)) //从select返回时,用FD_ISSET测试该集中的一个给定位是否仍旧设置
{
...
}
select函数的这三个参中的任一个(或全部)可以是空指针,这表示对相应的条件不关心。
值得一提的是:如果这三个指针全部为空,则select函数提供了比sleep更精确的计时器(sleep等待整数秒,而select函数可以等待少于1秒的时间,具体时间粒度取决于系统时钟),还有一个函数pselect更NB,可以提供ns的精度。
注意:
每次select函数超时返回后都要重新设置timeout的值,因为内核会把这个时间减少来计算何时函数返回。不然,一次超时返回后,timeout = 0。
通过select函数实现I/O多路转接:
采用select函数查询描述符集合中是否有描述符准备好相应操作,如果有则返回,进行操作;都没准备好则阻塞等待timeout时间,然后返回,避免了一直循环浪费CPU时间。