IO复用
TCP客户同时处理两个输入:标准输入(从键盘读入数据)和TCP套接字(读写套接字)。加入当客户阻塞于标准输入fgets时,服务器进程被杀死。此时服务器TCP给客户发送一个FIN。但是因为客户此时正阻塞于标准输入,所以不能及时读到这个FIN(还在套接字中)。一直到标准输入结束,到读套接字时,才知道服务器结束。这会导致时间的浪费。
对于IO复用来说,我们可以给进程指定一个或多个IO条件,内核一旦发现这些条件就绪(例如输入已准备好被读取,或描述符已能接受更多输出),就会通知进程。
IO复用一般用select和poll函数支持。
IO复用的使用场景
1)当使用多个描述符时,必须使用IO复用。
2)一个客户同时处理多个套接字
3)TCP服务器既要处理监听套接字,又要处理已连接套接字
4)一个服务器既要处理TCP,又要处理UDP
5)一个服务器要处理多个服务或多个协议。
select函数
该函数允许进程指示内核等待多个事件中的任意一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它。
select在可以在以下几种情况下返回:
指定几个描述符,准备好读;
指定几个描述符,准备好写;
指定几个描述符,有异常条件待处理;
已经经历了指定时间。
(1)函数原型
#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)
(2)timeout参数
strcut timeval{
long tv_sec; //秒
long tv_usec; //微秒
}
timeout该参数用于告诉内核等待所指定的描述符中的任何一个就绪可以花多少时间。
该参数有几种情况:
a、设置为空指针NULL。永远等下去,直到有描述符满足条件;
b、设置秒数。等待一段固定时间。在任何一个描述符准备好IO时才返回。
c、指定秒数和微秒数为0。不等待,检查描述符后马上返回,以轮询方式。
(3)描述符参数(集)
fd_set结构变量readset、writeset、exceptset用于指定我们让内核测试的读、写和异常条件的描述符集合。描述符集合fd_set的描述符总数是固定的FD_SETSIEZ=1024。但我们实际上不需要检测这么多的描述符。初始化时整个集合中全部位为0。
集合可以由四个宏来进行增减。也就是使其中描述符位为0或1。
void FD_ZERO(fd_set *feset); //使集合中全部位清零
void FD_SET(int fd, fd_set *fd_set); //使集合中某一位置为1
void FD_CLR(int fd, fd_set *fd_set); //使集合中某一位清零
void FD_ISSET(int fd, fd_set *fd_set); //检查集合中某一位是否为1,是返回1,否返回0
如果对其中任意一个不感兴趣,可以设置为NULL。
这三个参数都是值-结果参数。调用前fd_set中某些位置为1,表示我们关心这些描述符,随后传入select函数进行检测;调用后,准备就绪的位变为1,其他位变为0。我们检测哪些位为1,从而知道哪些描述符准备就绪。
函数调用前:
fd_set rset;
FD_ZERO(&rset);
FD_SET(1, &rset); //添加描述符,对应位置为1
FD_SET(4, &rset);
**函数返回后:**
FD_ISSET(1, &rset); //测试描述符是否就绪,检查对应位是否为1
(4)描述符参数个数
描述符集合fd_set的描述符总数是固定的FD_SETSIEZ=1024。但我们实际上不需要检测这么多的描述符。
因此设置int maxfdp1等于集合中待测试的最大描述符值+1。
例如集合中为1,4,5,则maxfdp1=6。
这个值实际上是所检测描述符的最大个数。描述符是从0开始的,因此1,4,5最多检测0,1,2,3,4,5共六个数。设置最大个数可以提高检测效率。
(5)返回值
调用成功:返回跨所有描述符集的已就绪的总位数,也就是读集、写集、异常集的就绪总和。
在任何描述符就绪前且定时还未结束:返回0.
出错:返回-1
(6)描述符就绪的条件
满足以下任何一个条件中的一个时,一个套接字准备好读:
a、缓冲区到标记位
套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。可以使用SO_RCVLOWAT套接字选项设置该值。
b、连接读半部关闭
该连接的读半部关闭。也就是接受了FIN的TCP连接。客户端关闭或服务端关闭,此时读取套接字不阻塞,且返回EOF。
c、监听套接字连接队列非0
如果一个监听套接字的已连接队列不为0,也就是它有可以读的已连接套接字,此时对这个套接调用accept不会阻塞。
d、套接字错误
某个套接字上有一个套接字错误待处理。此时读取该套接字不会阻塞,且返回-1,同时把errno设置成确切的错误条件。这些待处理错误可以通过指定SO_ERROR套接字选项,并调用getsockopt函数获取并清除。
满足以下任何一个条件中的一个时,一个套接字准备好写:
a、缓冲区到标记位
套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且,套接字已连接或不需要连接(UDP)。此时我们可以对这个套接字进行写。
b、连接写半部关闭
如果连接的写半部关闭,对这样的套接字进行写操作,会返回SIGPIPE信号。具体来说:客户端与服务端通信时,服务端突然断开连接,此时服务端再收到客户端的请求时,会回复一个RST。当一个进程向某个已收到RST的套接字执行写操作时,内核会向该进程发出一个SIGPIPE信号。
c、非阻塞connect
使用非阻塞connect的套接字且已经建立连接,或者conncet已经返回失败。
d、套接字错误
套接字上有套接字错误待处理,此时可以对套接字进行写操作,但是返回-1,且将errno置为确切的错误条件。
(6)套接字就绪总结
(7)select函数几个注意点
a、套接字错误时,select函数会将该套接字置为既可以读又可以写。
b、设置接收低水位和发送低水位目的:允许进程在select返回可读或可写条件之前,控制有多少数据可读或多少数据可写的空间大小。
使用select改写客户端IO函数
(1)使用阻塞式IO的客户端IO函数
//该函数用于发送客户端的域名请求并接收服务器的回复
void Send_Read_Client(FILE *fp,int clifd){
char sendbuf[1024];
char *end_flag="end";
while(fgets(sendbuf,1024,stdin)!=NULL){ //阻塞
sendbuf[strlen(sendbuf)-1]='\0';
if((write(clifd,sendbuf,strlen(sendbuf)))!=strlen(sendbuf)){
printf("write出错!");
exit(1);
}
if(bcmp(sendbuf,end_flag,strlen(sendbuf))==0)
break;
printf("成功发送sendbuf为:%s.\n等待服务器回应中……\n",sendbuf);
char recvbuf[1024];
int readbyte=-1;
if((readbyte=read(clifd,recvbuf,sizeof(recvbuf)-1))>0){ //阻塞至服务器回复
printf("读到的字节数为:%d\n",readbyte);
recvbuf[readbyte]='\0';
printf("收到服务器回复信息为:%s\n",recvbuf);
}
else{
printf("读取信息出错!");
exit(1);
}
}
}
该函数使用阻塞式IOfgets函数进行写操作。这个的问题在于,如果在通信过程中,服务器突然断开连接,此时发送FIN到客户端套接字,但是由于用户阻塞于fgets函数,没有进行read套接字的操作,因此客户端读不到这个EOF,因此客户端不会结束,直到fgets调用完成,进程调用read读到EOF。
因此可以使用select函数改写客户端读写函数。
(2)使用select监测套接字
阻塞式IO的问题在于,客户端会阻塞于标准IO或套接字的某一个,无法根据情况进行IO读写。
对于套接字来说,有三个信息应该获取:
异常信息:对端主机崩溃并重启,TCP发送RST,此时调用read返回-1
数据信息:TCP发送数据,套接字准备好读,此时调用read会返回大于0的值。
EOF信息:对端主机连接断开,TCP发送FIN,此时调用read返回0(EOF)。
(3)改写函数
void Send_Read_Serv(FILE *fp,int clifd){
int maxfdp1; //select函数最大描述符数
fd_set rset; //读套接字描述符集合
FD_ZERO(&rset); //全部清零
int fpno=fileno(fp); //输入文件流的描述符,这里是stdin标准IO
char sendbuf[1024]; //用来写入的服务器名
char *end_flag="end";
char recvbuf[1024]; //用来存储读到的服务器的回复信息
int readbyte=-1; //read函数的返回值
for(;;){
FD_SET(fpno,&rset); //fd_set置1,将stdin置为1
FD_SET(clifd,&rset); //将套接字描述符置为1
//将最大描述符数maxfdp1置为最大值加1
if(fpno>clifd)
maxfdp1=fpno+1;
else
maxfdp1=clifd+1;
//调用select监测两个描述符
if(select(maxfdp1,&rset,NULL,NULL,NULL)<0){
printf("调用select函数出错!");
exit(1);
}
//检查clifd是否置为1,置为1则表示上面三种情况之一发生,读就绪
if(FD_ISSET(clifd,&rset)){
//TCP回复数据
if((readbyte=read(clifd,recvbuf,sizeof(recvbuf)-1))>0){
printf("读到的字节数为:%d\n",readbyte);
recvbuf[readbyte]='\0';
printf("收到服务器回复信息为:%s\n",recvbuf);
}
//对端断开发送FIN
else if(readbyte==0){
printf("服务器连接已断开!");
exit(1);
}
//对端回复RST
else {
printf("连接异常!");
exit(1);
}
}
//检查标准输入是否置1,说明标准输入准备好读
if(FD_ISSET(fpno,&rset)){
if(fgets(sendbuf,1024,stdin)!=NULL){
sendbuf[strlen(sendbuf)-1]='\0'; //将回车键消化掉。
if((write(clifd,sendbuf,strlen(sendbuf)))!=strlen(sendbuf)) {
printf("write出错!");
exit(1);
}
//检查是否end
if(bcmp(sendbuf,end_flag,strlen(sendbuf))==0){
printf("连接已断开!");
break;
}
printf("成功发送sendbuf为:%s.\n",sendbuf);
}
}
}
}
select函数和stdio混用的后果
(1)标准IO(stdio)和系统IO的区别
stdio:在用户空间和内核空间都有缓存区。
系统IO(文件IO):只在内核空间有缓冲区。
(2)select和标准IO的问题
select函数的问题在于,它只对内核缓冲区具有判断读写是否就绪的能力。当我们通过stdin输入一行文本后,文本被刷入到内核缓冲区,此时select监测到stdin的文件描述符读就绪。此时就会调用fgets函数读取文本。fgets函数有用户缓冲区,它会将内核缓冲区中的文本全部刷入用户缓冲区,然后再进行读取。假设文本长度为10,但是我们定义的fgets函数中的buf长度只有3(去除’\0’只能读两个文本),那么读到前两个文本,select又会陷入阻塞。这是因为内核缓冲区的数据已经没有了,select认为stdin没有读就绪。然而实际上用户缓冲区中还有大量数据没有读完。
举例说明:
#include<stdio.h>
#include<sys/select.h>
int main(){
fd_set rfd;
char buf[3]={0};
FD_ZERO(&rfd);
while(1){
FD_SET(fileno(stdin),&rfd);
select(1, &rfd, NULL, NULL, NULL);
printf("standard input your data come in\n");
fgets(buf, 3, stdin);
printf("buf=%s\n",buf);
}
}
可以看到,第一次输入12456789后,读到12就陷入了阻塞。此时再次输入一行文本,fgets会先将之前用户缓冲区的文本读完,再将内核缓冲区的文本重新刷入。
(3)select和系统文件IO
使用系统文件IO不会出现这个问题。因为select和系统文件IO使用的都是内核缓冲区。select检测到内核缓冲区就绪时,调用文件IO(read)进行读写,read直接从内核缓冲区读取数据,读多少去掉多少。
#include<unistd.h>
#include<sys/select.h>
#include<stdio.h>
int main(){
fd_set rfs;
char buf[3]={0};
FD_ZERO(&rfs);
while(1){
FD_SET(fileno(stdin),&rfs);
select(fileno(stdin)+1,&rfs,NULL,NULL,NULL);
read(fileno(stdin),buf,3);
printf("buf=%s\n",buf);
}
}