《unix网络编程》(11)tcp服务器的几种常见状况分析的“服务器进程终止”提到客户阻塞于fgets所以没办法收到服务器发的FIN,只有当客户再次输入文本并发送给服务器后才会从套接字中读取,这时才知道服务器的状态。但这可能已经过了很长时间。这样的进程就需要预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(即,输入已经准备好读取,或描述符能够承接更多输出),它就通知进程。这种能力就是I/O复用(I/O mutiplexing)。该能力由select和poll支持。
select
select函数及参数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//返回:若有就绪描述符则返回就绪描述符的数目,若超时返回0,若出错返回-1
参数:
(1) timeout:告知内核等待指定描述符中任何一个就绪花费的最长时间,其timeval结构用于指定秒数和微妙数。
//timeval结构:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
使用select,设置其3个set为空指针, nfds为0, 一个非空的timeout来达到较为
精确的sleep(单位为微秒),普通的sleep单位为
秒。
timeout取值:
(1)timeout== NULL,仅当一个描述符准备好I/O时才返回。
(2)timeout->tv_sec == 0 && tvptr->tv_usec == 0,立即返回,称为 轮询( 非阻塞式I/O就是轮询)。
(3) timeout->tv_sec != 0 || tvptr->tv_usec !=0, 等待特定时间长度,超时返回0; 在这段时间内如果有描述符准备好就返回。
(2)中间的三个参数:指定要让内核测试读、写、异常的描述符,若对某一个不感兴趣可置为NULL。
这三个参数都是值-结果参数,调用函数时,用于指定所关心的描述符的值;函数返回时,结果将指示哪些描述符已经就绪。
select使用描述符集,通常是一个整数数组,其中每个整数的一位对应一个描述符。而poll是用可变长度的结构数组,每个结构代表一个描述符。
通过fd_set的数据类型和四个宏实现:
FD_CLR(int fd, fd_set *set); //关闭fd_set中的fd位
FD_ISSET(int fd, fd_set *set); //测试该位是否打开,如果为1则该位对应描述符就绪
FD_SET(int fd, fd_set *set); //打开该fd位
FD_ZERO(fd_set *set); //清空所有位
如下打开描述符1、4位:
fd_set rset;
FD_ZERO(&rset); //清空所有,每次调用select都要清空为0
FD_SET(1,&rset); //调用select将我们关心的位置为1.
FD_SET(4,&rset);
(3)maxfdp1参数指定待测试的描述符的个数,其值为最大待测试描述符加1。例如上例打开1、4描述符,那么这里maxfdp1值为5。
描述符就绪条件
当某套接字发生错误,将由select标记为既可读又可写。
shutdown函数
终止网络连接通常使用close函数。
不过close有两个限制,但可通过shutdown函数避免。
(1)close把描述符引用计数减1,仅在该计数为0时才关闭套接字。shutdown可以不管引用计数就激发TCP的正常连接终止。
(2)close终止读和写两个方向的数据传输。有时候需要告诉对端我们已经发完数据,即使对端仍有数据发送。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
//成功返回0,出错返回-1
shutdown允许我们处理批量数据,典型情况:
该函数行为依赖于howto参数:
(1)SHUT_RD
关闭连接读这一半——套接字不再有数据接收,而且套接字接收缓冲区现有数据丢弃。不再对这样的套接字调用读函数。来自对端的数据都被确认并丢弃。
(2)SHUT_WR
关闭连接写这一半,这称为半关闭。当前留在套接字缓冲区内的数据将被发送掉,后跟TCP的正常连接终止序列。
(3)SHUT_RDWR
连接的读和写都关闭。