《网络编程》非堵塞 I/O
概述
在前面文章中,我们介绍了 I/O 的五种模型《I/O 模型》。从那里能够知道,非堵塞式的 I/O 是进程调用 I/O 操作时。若数据未准备就绪。则马上返回一个 EWOULDBLOCK 错误。在数据准备就绪之前,应用进程採用轮询的方式检查数据是否准备就绪。直到数据准备就绪,则内核把该数据拷贝到应用进程的缓冲区,完毕数据复制之前进程处于堵塞状态,直到数据复制完毕后才返回。即
I/O 操作第一阶段处于轮询检查状态,第二阶段处于堵塞状态。
套接字的 I/O 操作默认状态是採用堵塞式。即当不能马上完毕套接字调用时,其进程会处于堵塞状态。直到对应操作完毕。堵塞套接字大致可分为下面四种类型:
- 输入操作引起的堵塞状态,包含 read、readv、recv、recvfrom 和 recvmsg 函数。
若某个进程对一个堵塞的 TCP 调用这些输入函数,且此时套接字的接收缓冲区没有可读数据。则该进程会处于堵塞状态。直到有一些数据到达。
对于字节流协议的 TCP 来说,仅仅要有一些数据到达。哪怕是单个字节数据。或者是完毕的 TCP 报文段数据,该进程都会被唤醒;对于数据报协议的 UDP 来说,若一个堵塞的 UDP 套接字接收缓冲区为空,则该进程也会处于堵塞状态,直到完整
UDP 数据报到达。对于非堵塞的套接字,若不能满足输入操作要求,则会马上返回一个 EWOULDBLOCK 错误。 - 输出操作引起的堵塞状态,包含 write、writev、send、sendto 和 sendmsg 函数。
对于一个堵塞 TCP 套接字,内核将从应用进程的缓冲区复制数据到该套接字的发送缓冲区,若该套接字的发送缓冲区没有存储空间,则进程会进入堵塞状态,直到有空间为止。对于一个非堵塞的 TCP 套接字。若其发送缓冲区没有存储空间,则输出函数调用马上返回一个 EWOULDBLOCK 错误。当发送缓冲区有空间时,则内核把应用进程缓冲区的数据拷贝到发送缓冲区中,并返回已复制的字节数。因为
UDP 套接字不存在真正的发送缓冲区。内核仅仅是将应用进程的数据复制并把它沿协议栈向下传送。因此对于一个堵塞 UDP 套接字调用输出函数时不会和 TCP 一样的原因而堵塞,是由于其它的原因(在书本上没有说明是什么原因)。 - 接受连接请求引起的堵塞状态。即 accept 函数。
当堵塞套接字调用 accept 函数时,若没有新的连接请求,则进程就会进入堵塞状态。非堵塞套接字调用 accept 函数时,且不存在新的连接请求。则 accept 函数调用马上返回一个 EWOULDBLOCK 错误;
- 发出连接请求引起的堵塞状态。即用于 TCP 的 connect 函数。
由于 TCP 的连接建立须要三次握手过程,且 connect 函数一直等待client收到对自己的 SYN 的应答响应 ACK 才返回。因此,每一个 connect 发起的连接请求在 RTT 时间内处于堵塞状态;对于 非堵塞 TCP 套接字调用 connect 函数时,若连接不能马上建立,则连接请求正常发出,可是会返回一个 EINPROGRESS 错误。但此时已经发起的 TCP 连接请求三次握手过程会继续进行。
非堵塞读写
在非堵塞读写的编程中,维护两个缓冲区:to 容纳从标准输入到server去的数据。fr 容纳自server到标准输出来的数据,这两个缓冲区详细结构例如以下图所看到的:
当中 toiptr 指针指向标准输入读入的数据能够存放的下一个字节,tooptr 指向下一个必须写到套接字的字节。一旦 tooptr 移动到 toiptr,则这两个指针就一起恢复到缓冲区開始处。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2hlbmhhbnpodW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
/* include nonb1 */
#include <sys/select.h>
#include <sys/socket.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #define MAXLINE 4096
inline int Max(int a, int b)
{
return(a >= b?a:b);
} extern void err_sys(const char *, ...);
extern void err_quit(const char *, ...); static void set_fl(int fd, int flags); void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[MAXLINE], fr[MAXLINE];
char *toiptr, *tooptr, *friptr, *froptr; /* 设置套接字描写叙述符为非堵塞 */
set_fl(sockfd, O_NONBLOCK);
/* 设置标准输入为非堵塞 */
set_fl(STDIN_FILENO, O_NONBLOCK);
/* 设置标准输出为非堵塞 */
set_fl(STDOUT_FILENO, O_NONBLOCK); /* 初始化两个缓冲区指针 */
toiptr = tooptr = to; /* initialize buffer pointers */
friptr = froptr = fr;
stdineof = 0;/* 标准输入键入EOF的标志 */ maxfdp1 = Max(Max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for ( ; ; ) {
/* 初始化。为调用select函数做准备 */
FD_ZERO(&rset);
FD_ZERO(&wset);
if (stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset); /* read from stdin */
if (friptr < &fr[MAXLINE])
FD_SET(sockfd, &rset); /* read from socket */
if (tooptr != toiptr)
FD_SET(sockfd, &wset); /* data to write to socket */
if (froptr != friptr)
FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */ if(select(maxfdp1, &rset, &wset, NULL, NULL) < 0)
err_sys("select error");
/* end nonb1 */
/* include nonb2 */
/* 若标准输入在rset有效。则从标准输入读取数据到发送缓冲区 */
if (FD_ISSET(STDIN_FILENO, &rset))
{
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; /* all done with stdin */
if (tooptr == toiptr)
if(shutdown(sockfd, SHUT_WR) < 0)/* send FIN */
err_sys("shutdown error"); }
else
{
toiptr += n; /* # just read */
FD_SET(sockfd, &wset); /* try and write to socket below */
}
} /* 若套接字在rset有效,则从套接字读取数据到接收缓冲区中 */
if (FD_ISSET(sockfd, &rset))
{
if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0)
{
if (errno != EWOULDBLOCK)
err_sys("read error on socket"); }
else if (n == 0)
{
if (stdineof)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely"); }
else
{
friptr += n; /* # just read */
FD_SET(STDOUT_FILENO, &wset); /* try and write below */
}
}
/* end nonb2 */
/* include nonb3 */
/* 若标准输出在wset有效,则从接收缓冲区写数据到标准输出 */
if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0))
{
if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0)
{
if (errno != EWOULDBLOCK)
err_sys("write error to stdout"); }
else
{
froptr += nwritten; /* # just written */
if (froptr == friptr)
froptr = friptr = fr; /* back to beginning of buffer */
}
} /* 若套接字在wset有效。则从发送缓冲区写数据到套接字中 */
if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0))
{
if ( (nwritten = write(sockfd, tooptr, n)) < 0)
{
if (errno != EWOULDBLOCK)
err_sys("write error to socket"); }
else
{
tooptr += nwritten; /* # just written */
if (tooptr == toiptr) {
toiptr = tooptr = to; /* back to beginning of buffer */
/* 若套接字接收来自标准输入的数据,则关闭套接字写端,相当于从标准输入键入EOF */
if (stdineof)
if(shutdown(sockfd, SHUT_WR) <0) /* send FIN */
err_sys("Shutdown error");
}
}
}
}
}
/* end nonb3 */ static void set_fl(int fd, int flags)
{
int val;
/* 获取描写叙述符状态标志 */
if( (val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl get error");
/* 加入描写叙述符状态标志flags*/
val |= flags; /* 设置描写叙述符状态标志 */
if(fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl set error");
}
非堵塞 connect
非堵塞 connect 有下面优点:
- 发起连接请求 connect时。在处于 TCP 连接三次握手过程的时间,即 RTT 时间内。套接字能够处理其它事务。不必处于堵塞状态;
- 在等待连接建立成功期间,套接字能够发起多个连接请求;
- 在非堵塞 connect 中使用到 select 函数,因此能够通过 select 參数自由设置超时时间。不必由系统限制 connect 的超时时间;
select 推断规则:
- 假设 select 返回 -1,表示 select 出错,能够关闭 socket 套接字。又一次发起连接过程;
- 假设 select 返回 0。表示在 select 超时。超时时间内未能成功建立连接。也能够再次运行 select 进行检測,如若多次超时。需返回超时错误给用户;
- 假设 select 返回大于 0 的值,则说明检測到可读或可写的套接字描写叙述符。
源自 Berkeley 的实现有两条与 select 和非堵塞 I/O 相关的规则:
- 1)当套接字连接建立成功时,套接口描写叙述符变成 可写(连接建立时,写缓冲区空暇,所以可写);
- 2)当套接字连接建立出错时。套接口描写叙述符变成 既可读又可写(因为有未决的错误,从而可读又可写);
因此,当发现套接口描写叙述符可读或可写时,可进一步推断是连接成功还是出错。这里必须将 2)和第二种连接正常的情况区分开,就是连接建立好了之后,server端发送了数据给client。此时 select 相同会返回非堵塞 socket 描写叙述符既可读又可写。
对于 Unix 环境,可通过调用 getsockopt 来检測描写叙述符集合是连接成功还是出错,可是该方法在 Linux 环境上測试是无效的。由于在 Linux 下不管网络是否错误发生,getsockopt 始终返回 0,不返回-1。
若採用 getsockopt 来检查:
- 假设连接建立是成功的,则通过 getsockopt(sockfd,SOL_SOCKET,SO_ERROR, &error,&len) 获取的 error 值将是 0。
- 假设建立连接时遇到错误,则 errno 的值是连接错误所相应的 errno 值,比方ECONNREFUSED,ETIMEDOUT 等;
在 Linux 环境下能够使用下面方法进行測试连接是成功还是出错:再次调用connect,对应返回失败。假设错误 errno 是EISCONN,表示 socket 连接已经建立。否则觉得连接失败。即在一次 select 调用之后。若发现此时套接口描写叙述字可读或可写,则再次运行 connect 调用。此时 errno 始终仍为 EINPROGRESS,则再次运行 select 和 connect 函数。直到 errno 被置为EISCONN,表示 connect
成功。
非堵塞 connect 编程步骤:
- 第一步:调用 socket 创建套接字。并使用 fcntl 函数使该套接字变为非堵塞式。
- 第二步:调用 connect 函数请求建立连接,并推断连接是否成功建立。
- 若 connect 调用返回 0。则表示连接请求成功建立;
- 若 connect 调用返回 -1。首先检查 errno 错误类型,若不为 EINPROGRESS 错误,则直接退出。否则仅仅是当前连接不能马上建立,可是已经发起的 TCP 连接请求三次握手过程会继续进行,此时调用 select 函数推断连接是否建立成功:
- 若 select 调用返回 0,则表示 select 超时期限内不能成功建立连接,则此时返回一个超时错误,且关闭该链接,以防止 TCP 连接的三次握手过程继续进行;
- 若 select 调用返回正值,则表示在超时期限内检查到套接字可读或可写或异常。若可读或可写。在 Unix 系统中,此时通过调用 getsockopt 函数检查连接状态。若连接成功。则该值为 0。若连接建立错误发生,则该值是相应连接错误的 errno 值;
如果在调用 select 函数之前连接已经建立,并server发送的数据已到达client。此时非堵塞套接字处于就可以读又可写状态。然而由 select 函数调用返回大于 0 值时。使用 getsockopt 检查到连接出错时。非堵塞套接字也是 既可读又可写。这样就会导致移植性问题,我们能够使用下列方法取代 getsockopt 调用:
- 调用 getpeername 取代 getsockopt。若 getpeername 以 ENOTCONN 错误失败返回,则表示连接建立失败,紧接着必须以 SO_ERROR 调用 getsockopt 取得套接字上待处理的错误;
- 以值为 0 的长度參数调用 read 函数。
若 read 调用失败,表示 connect 连接失败,read 返回的 errno 给出连接失败的原因,若连接建立成功,则 read 返回 0;
- 再一次调用 connect 函数。假设返回错误 EISCONN。表示套接字已经连接,即连接建立成功。
#include <sys/select.h>
#include <sys/socket.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> extern void err_quit(const char *, ...);
int
connect_nonb(int sockfd, const struct sockaddr *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;
/* 发起连接请求,若返回EINPROGRESS,表示连接建立已经启动可是尚未完毕 */
if ( (n = connect(sockfd, saptr, salen)) < 0)
if (errno != EINPROGRESS)/* 若是其它错误,则表示连接建立失败。直接退出 */
return(-1); /* Do whatever we want while the connect is taking place. */ /* 连接建立成功 */
if (n == 0)
goto done; /* connect completed immediately */ /* 若返回EINPROGRESS错误,表示连接建立已经启动可是尚未完毕;
* 此时调用select函数检查连接状态;
*/
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
/* 若select返回0。表示连接请求超时,建立连接失败,关闭该链接,并设置errno错误类型 */
if ( (n = select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return(-1);
}
/* 若select返回大于0,则此时套接字处于可读或可写状态。
* 则此时调用getsockopt函数检查连接状态,推断是连接成功还是失败;
*/
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 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);
}
非堵塞 accept
(1)使用 select 获悉某个监听套接字上何时有已完毕连接准备好被 accept 时。总是把这个监听套接字设置为非堵塞。
(2)在兴许的 accept 调用中忽略下面错误:EWOULDBLOCK(源自Berkeley的实现,客户终止连接时)。ECONNABORTED(POSIX实现,客户终止连接时)。EPROTO(SVR4实现,客户终止连接时)和 EINTR(假设有信号被捕获)
參考资料:
《Unix 网络编程》
《网络编程》非堵塞 I/O的更多相关文章
- UNIX网络编程——非阻塞connect:时间获取客户程序
#include "unp.h" int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec) ...
- UNIX网络编程-非阻塞connect和非阻塞accept
1.非阻塞connect 在看了很多资料之后,我自己的理解是:在socket发起一次连接的时候,这个过程需要一段时间来将三次握手的过程走完,如果在网络状况不好或者是其他的一些情况下,这个过程需要比较长 ...
- UNIX网络编程——非阻塞accept
当有一个已完成的连接准备好被accept时,select将作为可读描述符返回该连接的监听套接字.因此,如果我们使用select在某个监听套接字上等待一个外来连接,那就没有必要把监听套接字设置为非阻塞, ...
- UNIX网络编程——非阻塞connect: Web客户程序
非阻塞的connect的实现例子出自Netscape的Web客户程序.客户先建立一个与某个Web服务器的HTTP连接,再获取一个主页.该主页往往含有多个对于其他网页的引用.客户可以使用非阻塞conne ...
- UNIX网络编程——非阻塞connect
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三次握手继续进行.我们接着使用select检测这个连接或成功或失败的已建 ...
- UNIX网络编程——非阻塞式I/O(套接字)
套接字的默认状态是阻塞的.这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应的操作完成.可能阻塞的套接字调用可分为以下4类: (1)输入操作,包括read,readv,rec ...
- UNIX网络编程——并发服务器(TCP)
在迭代服务器中,服务器只能处理一个客户端的请求,如何同时服务多个客户端呢?在未讲到select/poll/epoll等高级IO之前,比较老土的办法是使用fork来实现. 网络服务器通常用fork来同时 ...
- UNIX网络编程卷1 时间获取程序client TCP 使用非堵塞connect
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.当在一个非堵塞的 TCP 套接字(可使用 fcntl 把套接字变成非堵塞的)上调用 co ...
- 网络编程----堵塞、非堵塞和同步、异步IO
我是学渣.但我想进步. 本文是面试我的牛人问我的.你知道什么是堵塞.非堵塞和同步.异步IO么?自觉得是分布式系统程序猿的我居然不知道.学习吧. 首先介绍堵塞IO和非堵塞IO: 堵塞IO:是指说程序等待 ...
随机推荐
- wap、app移动端页面常用html标签汇总
1.section 将内容组织到精确的语义块,表示页面的一部分. 2.article article表示网页的一个文章.故事. 3.header (1)用在整页的页头 (2)section或者arti ...
- c#:无法将类型为“System.DBNull”的对象强制转换为类型“System.String”
解决办法: 使用转换函数即可: Convert.ToString(要转换的值);
- Python 开发者的 6 个必备库,你都了解吗?
无论你是正在使用 Python 进行快速开发,还是在为 Python 桌面应用制作原生 UI ,或者是在优化现有的 Python 代码,以下这些 Python 项目都是应该使用的. Python那些事 ...
- Linux命令-目录处理命令:mv
注意:在linux下面,剪切文件和改名是同一个命令mv,而不是两个独立的命令. mv /tmp/beijing/chaoyangqu /root 移动chaoyangqu目录到root目录下面 mv ...
- jenkins二
破解管理员密码 1.假如我们忘记了Jenkins管理员密码了该怎么办呢?Jenkins没有用到数据库,所有的文件都是保存到xml文件里的 2.第一步找到admin所在的目录 [root@centos- ...
- FPGA管脚分配文件保存方法
使用别人的工程时,有时找不到他的管脚文件,但可以把他已经绑定好的管脚保存下来,输出到文件里. 方法一: 查看引脚绑定情况,quartus -> assignment -> Pins,打开F ...
- script标签的crossorigin属性
通常我们使用window.onerror来捕获js脚本的错误信息. 但是对于跨域调用的js脚本,onerror事件只会给出很少的报错信息:error: Script error. 这个简单的信息很明显 ...
- js 字符串拼接 html 累加 html 叠加
正常来说已经使用es6 的 模板了如`` //页面层 layer.open({ type: 1, content:`<div class="child_card"> & ...
- mysql innodb的重要组件
innodb包涵如下几个组件 一.innodb_buffer_pool: 1 它主要用来缓存数据与索引(准确的讲由于innodb中的表是由聚集索引组织的,所以数据只不是过主键这个索引的叶子结点). 二 ...
- //%f表示以十进制格式化输出浮点数 %.2f
//%f表示以十进制格式化输出浮点数 String s1 ="评分: %.1f"; String s2 = String.format(s1, 8.0); System.out.p ...