《UNIX网络编程 卷1》 笔记: 非阻塞式I/O

来源:互联网 发布:手机看本子的软件 编辑:程序博客网 时间:2024/05/22 15:35

本节讨论非阻塞式I/O操作。默认的I/O操作如readwrite函数等都是以阻塞的方式工作。比如read函数,如果没有数据可读取则进程一直阻塞直到有数据可读取。这种工作方式会带来某些问题。回到我们在I/O复用 select函数这一节实现的客户与服务器交互的str_cli函数,摘取其中处理标准输入的一段代码如下:

if (FD_ISSET(fileno(fp), &rset)) { /*标准输入可读*/  if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) { /*标准输入遇到EOF*/  stdineof = 1;  Shutdown(sockfd, SHUT_WR); /*关闭连接的发送端*/  FD_CLR(fileno(fp), &rset);  continue;  }  Writen(sockfd, buf, n);  }
进程从标准输入读取数据,然后调用Writen函数将数据通过套接字发送给服务器。问题是Writen函数(内部调用write函数)有可能会阻塞(比如发送缓冲区没有可用空间),此时如果套接字有来自服务器回射的数据,我们就无法及时处理。

为了解决这个问题,可以使用非阻塞式I/O操作,将标准输入、标准输出、套接字描述符都设置为非阻塞方式。这样I/O操作就不会阻塞,我们可以及时处理下一个事件。

为了实现非阻塞式I/O操作,我们需要维护两个缓冲区,to容纳从标准输入发往服务器的数据,fr容纳从服务器到标准输出的数据。以to缓冲区为,如下图所示:


从标准输入成功读取数据放到缓冲区时,toiptr指针前移。此时进程是生产者。

成功往服务器发送数据时tooptr指针前移。此时进程是消费者。

(toiptr -tooptr)为要发往服务器的数据,一旦没有要往服务器发送的数据,toiptr指针和tooptr指针指向缓冲区起始位置。

以to缓冲区为例,生产者的代码如下:

if (FD_ISSET(STDIN_FILENO, &rset)) { /*从标准输入读取数据放到to缓冲区*/if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {if (errno != EWOULDBLOCK)err_sys("read error on stdin");} else if (n == 0) {stdineof = 1;if (tooptr == toiptr)Shutdown(sockfd, SHUT_WR);} else {toiptr += n;FD_SET(sockfd, &wset);}}
消费者的代码如下:

if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0)) { /*从to缓冲区取出数据发送到服务器*/if ((nwritten = write(sockfd, tooptr, n)) < 0) {if (errno != EWOULDBLOCK)err_sys("write error to socket");} else {tooptr += nwritten;if (tooptr == toiptr) {toiptr = tooptr = to;if (stdineof)Shutdown(sockfd, SHUT_WR);}}}
读者在书中可以看到,整个str_cli函数的代码实现有90行左右,算是比较复杂的了。与多进程(或多线程)的版本比较,它的性能也只好那么一点,所以还是推荐使用多进程(或多线程)的方式。但是此例这种生产者和消费者的编程模型还是很重要的!

使用多进程的方式,我们还是使用阻塞式I/O,父进程处理标准输入到套接字的数据,子进程处理套接字到标准输出的数据,这样即使其中一个进程阻塞了也不会影响到另一个进程。代码如下:

void str_cli(FILE *fp, int sockfd){pid_t pid;char sendline[MAXLINE], recvline[MAXLINE];/*子进程 socket ---> stdout*/if ((pid = Fork()) == 0) {while (Readline(sockfd, recvline, MAXLINE) > 0)Fputs(recvline, stdout);/*子进程向父进程发送一个SIGTERM信号,以防止父进程继续运行*/kill(getppid(), SIGTERM);exit(0);}/*父进程stdin ---> socket*/while (Fgets(sendline, MAXLINE, fp) != NULL)Writen(sockfd, sendline, strlen(sendline));Shutdown(sockfd, SHUT_WR);pause();return;}
接下来我们来实现非阻塞connect函数。

发起一个非阻塞连接,有三个用途:
    1. 我们可以把等待三次握手完成的时间叠加在其他处理上。
    2. 我们可以使用这个技术同时建立多个连接。
    3. 我们可以使用select函数指定一个超时时间,缩短connect函数自身的超时。

注意的一点是select函数要监听建立连接的套接字描述符的读事件和写事件,因为连接成功时,描述符是可写的。连接失败时,描述符是可读和可写的。具体实现代码如下:

int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec){int flags, n, error;socklen_t len;fd_set rset, wset;struct timeval tval;/*将套接字描述符设置为非阻塞*/flags = Fcntl(sockfd, F_GETFL, 0);Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);error = 0;if ((n = connect(sockfd, saptr, salen)) < 0)if (errno != EINPROGRESS)  /*返回EINPROGRESS表示连接正在进行*/return -1;if (n == 0) /*连接完成*/goto done;/*当连接正在进行时,可以做其他事*/FD_ZERO(&rset);FD_SET(sockfd, &rset);wset = rset;tval.tv_sec = nsec;tval.tv_usec = 0;/*监听套接字描述符*/if ((n = Select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) { /*返回0 则连接超时*/close(sockfd);errno = ETIMEDOUT;return -1;}/*连接成功建立时, 套接字描述符变得可写;当连接建立遇到错误时, 描述符变为即可读又可写*/if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {len = sizeof(error);/*检查是否发生错误,错误值放在error*/if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)return -1;} elseerr_quit("select error:socket not set");done:/*恢复描述符标志*/Fcntl(sockfd, F_SETFL, flags);if (error) {close(sockfd);errno = error;return -1;}return 0;}

阅读全文
0 0
原创粉丝点击