内容简介: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) --彻底理解阻塞非阻塞与同步异步的区别
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python for Data Analysis
Wes McKinney / O'Reilly Media / 2012-11-1 / USD 39.99
Finding great data analysts is difficult. Despite the explosive growth of data in industries ranging from manufacturing and retail to high technology, finance, and healthcare, learning and accessing d......一起来看看 《Python for Data Analysis》 这本书的介绍吧!