内容简介:UNP 学习笔记——非阻塞式 IO
为什么要使用非阻塞式 IO?
- 对于边缘触发通知模式,非阻塞式 IO 可以最大程度的执行 IO 操作
- 对于水平触发模式,即使文件描述符已经就绪,如果我们在单个
read
或write
调用中写入足够大的数据,调用仍然可能会被阻塞 - 在非常罕见的情况下,水平触发模式会错误的通知我们文件描述符已经就绪了
- 如果多个进程或线程处理文件描述符,那么很可能在通知和调用 IO 之间,文件描述符的状态发生了改变
1、read
、write
如何使用非阻塞?
- 利用
fcntl
函数将fd
设置为O_NONBLOCK
- 利用
select
等 IO 复用方法等待socket
就绪 - 调用
read
、write
等非阻塞函数后,注意EWOULDBLCOK
错误,虽然很少会发生 - 处理
read
、write
等非阻塞函数返回 0 的情况,一般为FIN
结束标志 - 处理返回的数据
我们维护着两个缓冲区:to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。
其中 toiptr
指针指向从标准输入读入的数据可以存放的下一个字节,tooptr
指向下一个必须写到套接字的字节。有(toiptr-tooptr
)个字节需要写到套接字。可从标准输入读入的字节数是(&to[MAXLINE]-toiptr
)。一旦 tooptr
移动到 toiptr
,这两个指针就一起恢复到缓冲区开始处。
void str_cli( FILE *fp, int sockfd )
{
int maxfdp1, val, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[ MAX_MESG_SIZE ], fr[ MAX_MESG_SIZE ];
char *toiptr, *tooptr, *friptr, *froptr;
// 使用fcntl把所有3个描述符都设置为非阻塞,包括连接到服务器的套接字、标准输入和标准输出
val = fcntl( sockfd, F_GETFL, 0 );
fcntl( sockfd, F_SETFL, val | O_NONBLOCK );
val = fcntl( STDIN_FILENO, F_GETFL, 0 );
fcntl( STDIN_FILENO, F_SETFL, val | O_NONBLOCK );
val = fcntl( STDOUT_FILENO, F_GETFL, 0 );
fcntl( STDIN_FILENO, F_SETFL, val | O_NONBLOCK );
// 初始化指向两个缓冲区的指针,并把最大的描述符号加1,以用做select的第一个参数
toiptr = tooptr = to; // initialize buffer pointers
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max( max( STDIN_FILENO, STDOUT_FILENO ), sockfd ) + 1;
//这个版本的主循环也是一个select调用后跟对所关注各个条件所进行的单独测试
for( ; ; )
{
// 两个描述符集都先清零再打开最多2位。如果在标准输入上尚未读到EOF,而且在to缓冲区中有至少一个字节
// 的可用空间,那就打开描述符集中对应标准输入的位。如果在fr缓冲区中至少一个字节的可用空间,那就打
// 开描述符集中对应套接字的位。最后,如果在fr缓冲区中有要写到标准输出的数据,那就打开写描述符集中
// 对应标准输出的位
FD_ZERO( &rset );
FD_ZERO( &wset );
if( stdineof == 0 && toiptr < &to[ MAX_MESG_SIZE ] )
FD_SET( STDIN_FILENO, &rset ); // read from stdin
if( fripter < &fr[ MAX_MESG_SIZE ] )
FD_SET( sockfd, &rset ); // read from socket
if( tooptr != toiptr )
FD_SET( sockfd, &wset ); // data to write to sockfd
if( froptr != friptr )
FD_SET( STDOUT_FILENO, &wset ); // data to write to stdout
// 调用select,等待4个可能条件中任何一个变为真。我们没有为本select设置超时。
select( maxfdp1, &rset, &wset, NULL, NULL );
// 如果标准输入可读,那就调用read。指定的第三个参数是to缓冲区中的可用空间量
if( FD_ISSET( STDIN_FILENO, &rset ) )
{
if( ( n = read( STDIN_FILENO, toiptr, &to[ MAX_MESG_SIZE ] - toiptr ) ) < 0 )
{
// 如果发生一个EWOULDBLOCK错误,我们就忽略它。通常情况下这种条件“不应该发生”,因为这种条件意味着,
// select告知我们相应描述符可读,然而read该描述符却返回EWOULDBLOCK错误,不过我们无论如何还是处
// 理这种条件。
if( errno != EWOULDBLOCK )
{
printf( " read error on stdin\n " );
exit( 1 );
}
}
// 如果read返回0,那么标准输入处理就此结束,我们还设置stdineof标志。如果在to缓冲区中不再有数据要发送
// (即tooptr等于toiptr),那就调用shutdown发送FIN到服务器。如果在to缓冲区仍有数据要发送,FIN的发送
// 就得推迟到缓冲区中数据已写到套接字之后。
else if( n == 0 )
{
fprintf( stderr, " %s: EOF on stdin\n ", gf_time() );
stdineof = 1; // all done with stdin
if( tooptr == toiptr )
shutdown( sockfd, SHUT_WR ); // send FIN
}
// 当read返回数据时,我们相应地增加toiptr。我们还打开写描述符集中与套接字对应的位,使得以后在本循环
// 对应该位的测试为真,从而导致调用write写到套接字。
else
{
fprintf( srderr, " %s: read %d bytes from stdin\n ", gf_time(), n );
toiptr += n; // just read
FD_SET( sockfd, &wset ); // try and write to sockfd below
}
}
// 这段代码类似刚才讲解的处理标准输入可读条件的if语句。如果read返回EWOULDBLOCK错误,那么不做任何处理。
// 如果遇到来自服务器的EOF,那么若我们已经在标准输入上遇到EOF则没有问题,否则来自服务器的EOF并非预期。
// 如果read返回一些数据,我们就相应地增加friptr,并把写描述符集中与标准输出对应的位打开,以尝试在本函
// 数第三部分中将这些数据写到标准输出
if( FD_ISSET( sockfd, &rset ) )
{
if( ( n = read( sockfd, friptr, &fr[ MAX_MESG_SIZE ] - friptr ) ) < 0 )
{
if( errno != EWOULDBLOCK )
{
printf( " read error on socket\n " );
exit( 1 );
}
}
else if( n == 0 )
{
fprintf( stderr, " %s: EOF on stdin\n ", gf_time() );
if( stdineof )
return 0; // normal termination
else
{
printf( " str_cli: server terminated prematurely " );
exit( 1 );
}
}
else
{
fprintf( srderr, " %s: read %d bytes from socket\n ", gf_time(), n );
toiptr += n; // just read
FD_SET( sockfd, &wset ); // try and write to below
}
}
// 如果标准输出可写而且要写的字节数大于0,那就调用write。如果返回EWOULDBLCOK错误。那么不做任何处理。
// 注意这种条件完全可能发生,因为本函数第二部分末尾的代码在不清楚write是否会成功的前提下就打开了写描述
// 符集中与标准输出对应的位
if( FD_ISSET( STDOUT_FILENO, &wset ) && ( ( n = friptr - froptr ) > 0 ) )
{
if( ( nwritten = write( STDOUT_FILENO, froptr, n ) ) < 0 )
{
if( errno != EWOULDBLOCK )
{
printf( " write error to stdout\n " );
exit( 1 );
}
}
// 如果write成功,froptr就增加写处的字节数。如果输出指针(froptr)追上输入指针(friptr),这两个指针
// 就同时恢复为指向缓冲区开始
else
{
fprintf( stderr, " %s: wrote %d bytes to stdout\n ", gf_time(), nwritten );
froptr += nwritten; // just written
if( froptr == friptr )
froptr = friptr = fr; // back to beginning of buffer
}
}
// 这段代码类似刚才讲解的处理标准输出可写条件的if语句。唯一的差别是当输出指针追上输入指针时,不仅这两
// 个指针同时恢复到缓冲区开始处,而且如果已经在标准输入上遇到EOF就要发送FIN到服务器
if( FD_ISSET( sockfd, &wset ) && ( ( n = toiptr - tooptr ) > 0 ) )
{
if( ( nwritten = write( sockfd, tooptr, n ) ) < 0 )
{
if( errno != EWOULDBLOCK )
{
printf( " write error to socket\n " );
exit( 1 );
}
}
else
{
fprintf( stderr, " %s: wrote %d bytes to socket\n ", gf_time(), nwritten );
tooptr += nwritten; // just written
if( tooptr == toiptr )
toiptr = tooptr = to; // back to beginning of buffer
if( stdineof )
shutdown( sockfd, SHUT_WR ); // send FIN
}
}
}
}
2、非阻塞 connect
的优点有哪些?
- 我们可以在三路握手的同时做一些其它的处理.
connect
操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网.在这段时间内我们可能有一些其他的处理想要执行; - 可以用这种技术同时建立多个连接.在Web浏览器中很普遍;
- 由于我们使用select来等待连接的完成,因此我们可以给
select
设置一个时间限制,从而缩短connect
的超时时间.在大多数实现中,connect
的超时时间在75秒到几分钟之间.有时候应用程序想要一个更短的超时时间,使用非阻塞connect
就是一种方法;
3、非阻塞 connect
需要处理的问题有哪些?
- 即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;
- 源自
Berkeley
的实现(和Posix.1g
)有两条与select
和非阻塞 IO 相关的规则:- 当连接建立成功时,套接口描述符变成可写;
- 当连接出错时,套接口描述符变成既可读又可写;
因此,仅从socket可读或可写无法判断socket连接的状态。
#include "unp.h"
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)//连接不能立即建立,已经发起的三次握手继续进行,同时返回-1,errno设置为EINPROGRESS
if (errno != EINPROGRESS)
return(-1);
/* Do whatever we want while the connect is taking place. */
if (n == 0)//返回成功,连接能够立即建立
goto done; /* connect completed immediately */
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) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return(-1);
}
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)//error返回0,表示在等待的时间内连接建立;如果非零,表示连接建立遇到了套接字错误而导致的套接字可读可写
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");
done:
fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
if (error) {
close(sockfd); /* just in case */
errno = error;
return(-1);
}
return(0);
}
非阻塞 accecpt
的优点?
struct linger
的 l_onoff
标志设为 1,l_linger
设为 0。这个时候,如果关闭 TCP
连接时,会先在 socket
上发送一个RST
包。这个时候会出现下面的问题:
- select向服务器返回监听socket可读,但是服务器要在一段时间之后才能调用accept;
- 在服务器从select返回和调用accept之前,收到从客户发送过来的RST;
- 这个已经完成的连接被从队列中删除,我们假设没有其它已完成的连接存在;
- 服务器调用accept,但是由于没有其它已完成的连接存在,因而服务器被阻塞了;
解决这个问题的办法是:
- 如果使用select来获知何时有链接已就绪可以accept时,总是把监听socket设置为非阻塞模式,并且
- 在后面的accept调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现在客户放弃连接时出现的错误)、ECONNABORTED(Posix.1g的实现在客户放弃连接时出现的错误)、EPROTO(SVR4的实现在客户放弃连接时出现的错误)和EINTR(如果信号被捕获).
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Node.js 指南(阻塞与非阻塞概述)
- Node.js 回调函数 阻塞与非阻塞
- 明明白白学 同步、异步、阻塞与非阻塞
- 从 Linux 源码看 socket 的阻塞和非阻塞
- 分布式系统关注点——阻塞与非阻塞有什么区别?
- Netty基础系列(2) --彻底理解阻塞非阻塞与同步异步的区别
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Looking For a Challenge
the University of Warsaw / the University of Warsaw / 2012-11 / 0
LOOKING FOR PROGRAMMING CHALLENGES? Then this book is for you! Whether it's the feeling of great satisfaction from solving a complex problem set, or the frustration of being unable to solve a task,......一起来看看 《Looking For a Challenge》 这本书的介绍吧!