UNIX网络编程笔记(6):I/O复用之select函数

来源:互联网 发布:社交网络 720p mkv 编辑:程序博客网 时间:2024/06/05 11:12

上一讲中我们正确处理了僵尸子进程,使得这个简单的服务器更加健壮。不幸的是,这个程序仍然有问题。想象一下,如果一个客户正在和一个服务器子进程连接建立完毕正在通话,而服务器子进程意外终止(比如kill),服务器TCP向客户TCP发送一个FIN,但客户端正在调用fgets函数等待用户输入字符而得不到这个FIN,直到套接字读为止(可能过了很长时间)。

1、来看看问题是什么

首先启动服务器与客户,建立连接:


客户输入nihao后服务器正确回射,然后kill掉服务器子进程后客户输入and...,输出错误:


服务器端重启(上一讲中解决的问题)。

这个过程发生了什么?

(1)当kill掉服务器子进程后,服务器父进程正确处理了SIGCHLD信号。服务器也向客户发送一个FIN,客户回应一个ACK,TCP连接终止的前半部分完成;

(2)客户上没有发生任何特殊的事,客户TCP接收到服务器的FIN后回应一个ACK,但问题是客户正阻塞在fgets上,等待用户输入;

(3)在客户上再输入and...,导致错误发生:str_cli调用write,客户TCP接着把数据发送给服务器,但是客户FIN的接收并没有告知客户TCP服务器进程已经终止。当服务器TCP接收到客户的数据时,发送一个RST;

(4)但是客户进程看不到这个RST,因为调用write后直接调用readline,并且由于之前接收的FIN,导致readline直接返回0(表示EOF)。但客户没有预期收到EOF,于是出错;

(5)客户终止,所有打开的描述符关闭;

问题的根本在于,当FIN到达套接字时,客户正阻塞在fgets调用上。客户实际上在应对两个描述符:套接字和用户输入,它不能单纯阻塞在某个特定的源上。select函数可以解决这个问题。

2、select函数

有些进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪时,它就通知进程。这个能力就是I/O复用。

select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

下面的例子指出select可以告知内核在什么条件下发生时返回:

  • 集合{1,4,5}中的任何描述符准备好读;
  • 集合{2,7}中的任何描述符准备好写;
  • 集合{1,4}中的任何描述符由异常条件待处理;
  • 已经过了12.8秒;
也就是说,调用select函数告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。描述符不局限于套接字,任何描述符都可以使用select。
函数定义如下,包含在<sys/select.h>头文件中:
#include <sys/select.h>#include <sys/time.h>int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set * exceptset,const struct timeval *timeout);
函数如果有描述符就绪就返回数量,超时返回0,出错返回-1。
参数含义如下:
maxfdp1:指定待测试描述符的个数,通常是待测试的最大描述符加1;
中间三个参数指定内核测试读、写和异常条件的描述符,这里设置成NULL即可;
timeval:告知内核等待所指定的时间,有三种情况:
(1)永远等待:设置为NULL;
(2)等待固定时间:在有一个描述符就绪后返回,但不超过参数所指定的时间;
(3)根本不等待:检查描述符后立刻返回;
3、重写客户端中的str_cli函数
客户的套接字上的三个条件:
(1)如果对端TCP发送数据,那么该套接字变为可读,并且read返回一个大于0的值(即读入数据的字节数);
(2)如果对端TCP发送一个FIN,那么该套接字变为可读,read返回0(EOF);
(3)如果对端TCP发送一个RST(对端主机崩溃并重启),那么该套接字变为可读,并且read返回-1,errno中有错误码;
下面是修改后的str_cli函数:
void str_cli(FILE *fp,int sockfd){int maxfdp1;fd_set rset;char sendline[MAXLINE],recvline[MAXLINE];FD_ZERO(&rset);int n;for(;;){FD_SET(fileno(fp),&rset);FD_SET(sockfd,&rset);maxfdp1=max(fileno(fp),sockfd)+1;if((n=select(maxfdp1,&rset,NULL,NULL,NULL))==0){printf("select timeout\n");return;}else if(n<0){printf("select error\n");return;}if(FD_ISSET(sockfd,&rset)){if(readline(sockfd,recvline,MAXLINE)==0){printf("str_cli:server terminated prematurely\n");return;}fputs(recvline,stdout);}if(FD_ISSET(fileno(fp),&rset)){if(fgets(sendline,MAXLINE,fp)==NULL)return;write(sockfd,sendline,strlen(sendline));}}}
函数首先检查套接字,如果套接字是可读的,那么就用readline读入回射文本并输出;如果标准输入是可读的,就调用fgets读入一行,然后写入套接字。
运行结果:
(1)服务器进程终止后客户端直接终止:


(2)服务器端:

可以看到,程序运行良好。

1 0
原创粉丝点击