本文主要是介绍第十六章 非阻塞I/O,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
第十六章、非阻塞式I/O什么是阻塞socket和非阻塞socket?两者的具体区别是什么?
读操作
对于阻塞的socket,当socket的接收缓冲区中没有数据时,read调用会一直阻塞住,直到有数据到来才返回。当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。
对于非阻塞socket而言,socket的接收缓冲区中有没有数据,read调用都会立刻返回。接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的,如果接收缓冲区中没有数据,则返回错误号EWOULDBLOCK,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回,遇到这样的情况,可以在下次接着去尝试读取。如果返回值是其它负值,则表明读取错误。
写操作
对于写操作write,原理是类似的,非阻塞socket在发送缓冲区没有空间时会直接返回错误号EWOULDBLOCK,表示没有空间可写数据,如果错误号是别的值,则表明发送失败。如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
而对于阻塞Socket而言,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回.
可能阻塞的套接字调用可分为以下四类:
1、输入操作(read、readv,recv,recvfrom,recvmsg)。如果某个进程对一个阻塞的TCP套接字调用这些函数之一,而且该套接字的接收缓冲区中没有数据可读,那么该进程将被投入睡眠,直到有一些数据到达(可以是单个字节也能是完整的TCP分节中数据)
对于非阻塞的套接字,如果输入不能被满足(对于TCP即至少一个字节可读,对于UDP要求有一个完整的数据报可读),则返回EWOULDBLOCK
2、输出操作(write,writev,send,sendto,sendmsg)。对于一个TCP套接字,内核将从应用进程缓冲区到该套接字的缓冲区复制数据。对于阻塞的套接字,如果发现其缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。
对于一个非阻塞的套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回EWOULDBLOCK错误;如果发现缓冲区中有一些空间,返回值将是内核能够复制到该缓冲区中的字节数,也称为不足计数(short count)
不过,UDP套接字不存在真正的发送缓冲区,内核只是复制应用进程数据并把它沿协议栈向下传送,渐次冠以UDP首部和IP首部。因此对于UDP套接字,输出函数调用将不会因与TCP一样的原因为阻塞(不过有可能因其他原因阻塞)
对于以上对于阻塞与非阻塞套接字的理解,我们重写str_cli函数:
函数中的发送缓冲区 to 与接收缓冲区 fr
#include "unp.h"#define oops(m) {perror(m);exit(1);}
#define BUFSIZE 4096void str_cli(FILE* ,int);int main(int ac,char *av[])
{int sockfd;setup(sockfd);str_cli(stdin, sockfd);exit(0);
}vodi str_cli(FILE *fp, int fd)
{int flag, maxfdpl;int stdineof;fd_set wset, rset;//" to " buffer is 发送缓冲区 " fr " buffer is 接收缓冲区char to[BUFSIZE], fr[BUFSIZE];//toiptr is current point, tooptr is the startchar *toiptr, *tooptr, *friptr, *froptr;ssize_t n, nwriten;//------------------------------------------------set O_NONBLOCKflag = fcntl(fd, F_GETFL);fcntl(fd,F_SETFL);flag = fcntl(STDIN_FILENO, F_GETFL);fcntl(fd,F_SETFL);flag = fcntl(STDOUT_FILENO, F_GETFL);fcntl(fd,F_SETFL);toiptr = tooptr = to;friptr = froptr = fr;stdineof = 0;maxfdpl = fd+1;//---------------------------------------------start loopfor(;;){FD_ZERO(&wset);FD_ZERO(&rset);//choose descriptors to wait for//if [input is over or buffer is full] , cannot read from stdinif(stdineof==0 && toiptr<&to[BUFSIZE])FD_SET(STDIN_FILENO,&rset);//if [fr is empty], cannot write into anybodyif(friptr != froptr)FD_SET(STDOUT_FILENO,&wset);//if [to buffer is empty], cannot write into fdif(toiptr != tooptr)FD_SET(fd,&wset);//if [fr buffer is full], cannot wait for fd to readif(friptr < &fr[BUFSIZE])FD_SET(fd,&rset);if(select(maxfdpl,&rset,&wset,NULL,NULL) < 0) //OK to call selectoops("select error");if(FD_ISSET(STDIN_FILENO,&rset)) //can read from stdin{if((n=read(STDIN_FILENO,toiptr,&to[BUFSIZE]-toiptr)) < 0){if(errno != EWOULDBLOCK)oops("read error");}else if( n == 0 ){printf("EOF on stdin\n");stdineof = 1; //all done with stdinif(toiptr == tooptr) //接收到EOF只能表明应用程序结束发送数据,并不表示套接字缓冲区数据已经全部发送。所以如果此刻正好缓冲区也为空,那么我们就能向服务器发送FIN了shutdown(fd,SHUT_WR);}else{printf("%d bytes read from stdin\n",n);toiptr += n;FD_SET(fd,&wset); //not just for next loop, buf for the next [ if(FD_ISSET(fd,&wset)) ];我们不再等到下一次循环判定,希望直接在接下来的程序中向fd写数据}}if(FD_ISSET(fd,&rset)) {if((n=read(fd,friptr,&fr[BUFSIZE]-friptr)) < 0){if(errno != EWOULDBLOCK)oops("read error");}else if(n == 0){if(stdineof != 1){fprintf(stderr,"Unexpected EOF");exit(1);}}else{printf("%d bytes read from fd\n",n);friptr += n;FD_SET(STDOUT_FILENO,&wset);}}if(FD_ISSET(STDOUT_FILENO,&wset) && ((n=friptr-froptr)>0)) //may comes from [if(FD_ISSET(fd,&rset))]{//非阻塞write与阻塞write的不同之处在于,当我们要求写向某处,但该处此刻不一定有(足够的)空间可以被写入;若是阻塞模式那么必然会等待,从而在write处阻塞,而非阻塞模式下write则不会再等待而是返回EWOULDBLOCK立刻退出不再等待if((nwriten=write(STDOUT_FILENO,froptr,n)) < 0) // n comes from [ if(FD_ISSET(fd,&rset)) ]{if(errno != EWOULDBLOCK)oops("write error");}else{froptr += nwriten;if(friptr == froptr)friptr = froptr = fr;}}if(FD_ISSET(fd,&wset) && ((n=toiptr-tooptr)>0)){if((nwriten=write(fd,tooptr,n)) < 0){if(errno != EWOULDBLOCK)oops("write error into socket");}else{tooptr += nwriten;if(tooptr == toiptr){tooptr = toiptr = to;if(stdineof) //如果此刻正好为空,且应用程序已发出EOF,那么可先关闭一方shutdown(fd,SHUT_WR);}}}}
}
若将此程序输入输出皆指向文件,那么会出现下图情况:
为什么要改为非阻塞I/O呢?有哪些更好的地方呢?
结合下面贴出的之前的 select+阻塞套接字 str_cli函数。
举例来说,如果在标准输入中有一行文本可读,我们就调用read读入它,再调用writen把它发送给服务器。然而如果套接字发送缓冲区已满, writen调用将会阻塞。在进程阻塞于writen期间,可能有来自套接字接收缓冲区的数据可供读取。类似的,如果从套接字中有一行输入文本可读,那么一旦标准输出比网络还要慢,进程照样可能阻塞于后续的write。
因此, 改为非阻塞IO的是为了防止进程在任何可以做任何有效工作期间发生阻塞。
#include "unp.h"#define oops(m) {perror(m);exit(1);}void str_cli(FILE* ,int);int main(int ac,char *av[])
{int sockfd;set_up(sockfd);str_cli(stdin, sockfd);exit(0);
}void str_cli(FILE *fp, int sockfd)
{int maxfdlp, stdineof;fd_set rset;int nbyte;FD_ZERO(&rset);stdineof = 0;for(;;){maxfdlp = max(fileno(fp),sockfd)+1;if(stdinof == 0)FD_SET(fileno(fp), &rset);FD_SET(sockfd, &rset);if(select(maxfdlp,&rset,NULL,NULL,NULL) < 0){perror("select");exit(1);}if(FD_ISSET(sockfd, &rset)){if((nbyte=read(sockfd,buf,MAXLEN)) == 0){if(stdineof == 1)return; //normal endelse{ //service send FIN to this client unexpectedlyfprintf(stderr,"Service Terminated Unexpected\n");exit(1);}}else if(nbyte < 0){perror("Read Error");exit(1);}if(write(1,buf,nbyte) != nbyte){perror("Write Error");exit(1);}}if(FD_SET(fileno(fp),&rset)){if((nbyte=read(fileno(fp),buf,MAXLEN)) == 0){stdineof = 1;shutdown(sockfd, SHUT_WR); //send FIN to service to close half-TCP about write//which means we would not send data anymore.FD_CLR(fileno(fp),&rset);continue;}else if(nbyte < 0){perror("Read Error");exit(1);}if(written(sockfd,buf,nbyte) != nbyte){perror("Write Error");exit(1);}}}
}
这两个程序相比较,我个人觉得,问题主要在于两个write可能会互相阻塞等待,因此在非阻塞的情况下可以提高性能。但是代码却较之前增加了太多。
以下为书上的简易却实用的版本:
void str_cli(FILE *fp, int fd)
{pid_t pid;char sendline[BUFSIZE];if((pid=fork()) < 0)oops("error");if(pid == 0) //child : serv ---> fd{while(Readline(sockfd, recvline, MAXLINE) > 0)Fputs(recvline, stdout);kill(getppid(), SIGTERM); //服务器可能故障停止,子进程接收到unexpected的EOF,然而父进程仍然在从fp复制数据到套接字,此刻子进程必须向父进程发送SINTERM从而也停止向服务器发送任何数据exit(0);}while(Fgets(sendline, MAXLINE, fp) != NULL)Writen(sockfd, sendline, strlen(sendline));Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
}
我们早已知道TCP是全双工的,这里父子进程共享同一套接字。尽管发送缓冲区和接收缓冲区只有一个,但却有两个描述符在引用它,一个在父进程,另一个在子进程
之前的非阻塞版本同时管理4个流,而且由于都是非阻塞的,不得不考虑4个流的部分读和部分写问题。然而在此版本,每个进程处理两个流,从一个复制到另一个。这里不许要非阻塞, 【因为如果从输入了没有数据可读,那么输出流就没有数据可写】(理解这句话可以帮助理解之前的程序)
这篇关于第十六章 非阻塞I/O的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!