《Unix 网络编程》16:非阻塞式 I/O
非阻塞式 I/O

本文信息 | 本文信息 | 防爬虫替换信息 |
---|---|---|
作者网站 | LYMTICS | https://lymtics.top |
作者 | LYMTICS(樵仙) | https://lymtics.top |
联系方式 | me@tencent.ml | me@tencent.ml |
原文标题 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 |
原文地址 | https://www.cnblogs.com/lymtics/p/16364065.html | https://www.cnblogs.com/lymtics/p/16364065.html |
- 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
- 原文会不断地更新和完善,排版和样式会更加适合阅读,并且有相关配图
- 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息

系列文章导航:《Unix 网络编程》笔记
阅读此文时,如果有哪一段看不懂了,请务必回忆一下所谓“非阻塞”的含义!
概述

本文信息 | 本文信息 | 防爬虫替换信息 |
---|---|---|
作者网站 | LYMTICS | https://lymtics.top |
作者 | LYMTICS(樵仙) | https://lymtics.top |
联系方式 | me@tencent.ml | me@tencent.ml |
原文标题 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 |
原文地址 | https://www.cnblogs.com/lymtics/p/16364065.html | https://www.cnblogs.com/lymtics/p/16364065.html |
- 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
- 原文会不断地更新和完善,排版和样式会更加适合阅读,并且有相关配图
- 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息

套接字的默认状态是阻塞的,这意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待响应的操作完成。
可能阻塞的套接字调用可以分为如下四类:
(如果不清晰可以尝试放大查看)
注意,返回一个错误并不意味着程序会退出执行,实际上是设置了一个标志位,我们可以忽略这个错误
另外,我们可以之后用 select 来看相应的操作是否完成
非阻塞读写

本文信息 | 本文信息 | 防爬虫替换信息 |
---|---|---|
作者网站 | LYMTICS | https://lymtics.top |
作者 | LYMTICS(樵仙) | https://lymtics.top |
联系方式 | me@tencent.ml | me@tencent.ml |
原文标题 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 |
原文地址 | https://www.cnblogs.com/lymtics/p/16364065.html | https://www.cnblogs.com/lymtics/p/16364065.html |
- 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
- 原文会不断地更新和完善,排版和样式会更加适合阅读,并且有相关配图
- 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息

非阻塞 IO 版本
预备
回忆之前的版本:
我们之前在 IO 多路复用中,用 SELECT 改进了我们 str_cli 的代码,使得它可以同时监听多个描述符的事件,但是,这仍然属于阻塞式 IO,其在发生阻塞时的行为和我们上面图片展示的相似。
我们要写的版本的注意事项:
- 非阻塞 IO 的加入让缓冲区管理显著地复杂化了
- 在套接字上使用标准 IO 存在潜在的问题和困难,所以我们将继续避免使用它们
缓冲区模型:
我们要维护两个缓冲区:
to
容纳从标准输入到服务器去的数据fr
容纳从服务器到标准输出的数据
如下图所示:
代码
一段超级长的代码,但是只要记住它的大概流程就可以更好地理解整个程序:
- 首先是初始化代码,包括设置描述符为非阻塞、select 相关的初始化、前文提到的几个计数指针
- 然后是对 select 监听的各种事件的处理:标准输入可读、套接字可读、标准输出可写、套接字可写
- 在每个事件的处理中,要尝试读取数据,并根据是否成功做进一步的操作
strclinonb.c
void str_cli(FILE* fp, int sockfd) {
int maxfdp1, val, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[MAXLINE], fr[MAXLINE];
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(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);
// 初始化上图中提到的那几个指针
toiptr = tooptr = to;
friptr = froptr = fr;
stdineof = 0;
// 计算最大描述符,并加一,之后调用 select 时会用到
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 */
// 等待上述条件之一为真,这里我们没有设置超时
Select(maxfdp1, &rset, &wset, NULL, NULL);
/* end nonb1 */
/* include nonb2 */
// 如果标准输入可读
if (FD_ISSET(STDIN_FILENO, &rset)) {
// 尝试读取,注意我们现在已经使用非阻塞模式了
if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
// 我们忽略了 EWOULDBLOCK 错误,因为这通常情况下不应该发生
// (这意味着 select 告诉我们可读, read 却读取失败)
if (errno != EWOULDBLOCK)
// 读取失败,报错
err_sys("read error on stdin");
// n == 0 说明标准输入处理到此结束
} else if (n == 0) {
// 输出信息和时间,以表示这个 EOF
fprintf(stderr, "%s: EOF on stdin\n", gf_time());
// 设置 stdineof = 1, 表示输入结束
// 后面会根据此标志进行关闭等操作
stdineof = 1; /* all done with stdin */
if (tooptr == toiptr)
Shutdown(sockfd, SHUT_WR); /* send FIN */
// 读取到了数据
} else {
fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);
toiptr += n; /* # just read */
// 打开套接字相关的描述符
FD_SET(sockfd, &wset); /* try and write to socket below */
}
}
// 如果 socket 可读,注意上面5行前设置了这个标志
if (FD_ISSET(sockfd, &rset)) {
// 尝试读取
if ((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
// 如果是 EWOULDBLOCK, 则不阻塞,而重新进入下一个 if 中
if (errno != EWOULDBLOCK)
err_sys("read error on socket");
} else if (n == 0) {
fprintf(stderr, "%s: EOF on socket\n", gf_time());
// 如果前面设置了这个标志,说明是从标准输入读入了 EOF
// 所以正常结束
if (stdineof)
return; /* normal termination */
// 否则,报错
else
err_quit("str_cli: server terminated prematurely");
} else {
fprintf(stderr, "%s: read %d bytes from socket\n", gf_time(), n);
friptr += n; /* # just read */
FD_SET(STDOUT_FILENO, &wset); /* try and write below */
}
}
/* end nonb2 */
/* include nonb3 */
// 标准输出是否准备就绪,并且有可以写的数据,注意这里有两个判断条件
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 {
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 */
}
}
// 检查 socket 是否可写,并且有能够发送的
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 {
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 */
// 由于 tooptr == toiptr ,所以没有需要发送的数据了
// 检查标志,如果用户输入了 EOF,就发送一个 FIN
if (stdineof)
Shutdown(sockfd, SHUT_WR); /* send FIN */
}
}
}
}
}
/* end nonb3 */
其中使用到了一个可以返回当前时间的函数,代码如下:
gf_time.c
char* gf_time(void) {
struct timeval tv;
time_t t;
static char str[30];
char* ptr;
if (gettimeofday(&tv, NULL) < 0)
err_sys("gettimeofday error");
t = tv.tv_sec; /* POSIX says tv.tv_sec is time_t; some BSDs don't agree. */
ptr = ctime(&t);
strcpy(str, &ptr[11]);
/* Fri Sep 13 00:00:00 1986\n\0 */
/* 0123456789012345678901234 5 */
snprintf(str + 8, sizeof(str) - 8, ".%06ld", tv.tv_usec);
return (str);
}
运行
原文使用了 tcpdump 进行操作,但是由于我没有学习这个,并且按照原文的方法无法正确使用,所以这里只列出相关的操作,之后再做补充
用如下命令捕获去往或来自端口 7 的TCP分节,输出存储在 tcpd 中:
tcpdump -w tcpd tcp and port 7
运行服务器程序,对应在 tcpcliserv
目录下的任一服务器代码,记得修改端口
运行客户端程序,其中 2000.lines
是一个 2000 行的文件:
tcpcli02 127.0.0.1 < 2000.lines > out 2> diag
最后,查看区别:
diff 2000.lines out
如下是其的时间线:
多进程版本
思考
你可能也感觉到了,上面的代码十分复杂,为了程序的效率而提升代码的复杂度可能是得不偿失的。
每当我们发现需要使用非阻塞 IO 时,更简单的方法是把应用程序任务划分到多个进程或多个线程中。
多进程代码
void str_cli(FILE* fp, int sockfd) {
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if ((pid = Fork()) == 0) { /* child: server -> stdout */
while (Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
kill(getppid(), SIGTERM); /* in case parent still running */
exit(0);
}
/* parent: stdin -> server */
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
pause();
return;
}
注意点如下:
-
父子进程可以共享一个套接字,以及接收和发送缓冲区
-
当父进程读取到 EOF 时,调用 Shutdown 而不是 Close,从而子进程可以继续正常工作
-
如果服务器方面断开连接,则子进程有必要告诉父进程不要再发了,所以子进程发送 SIGTERM 信号
另一种方法是子进程终止,使得父进程捕获一个 SIGTERM 信号
-
父进程读取完数据后,会调用 pause 等待子进程完成;子进程完成会发送一个 SIGTERM 信号,由于我们没有捕捉这个信号,所以其默认行为为终止;这样做的目的是为了统计时间
总结
我们已经编写了 str_cli 函数的四个不同的版本,总结如下:
版本 | 对上一版本的改进 | 运行时间 | 代码量 |
---|---|---|---|
停等版本 | - | 354s | 12行 |
select 加阻塞版本 | 像流水线一样发送 用 select 同时监听套接字和文件描述符 |
12.3s | 40行 |
非阻塞 IO 版本(本章) | 缓冲区不可用时直接进入下一轮循环 不会因为要等待缓冲区的准备而浪费时间 |
6.9s | 130行 |
fork 版本(本章) | 简化了代码的逻辑 缓冲区分给了不同的进程,不需要使用非阻塞 IO |
8.5s | 20行 |
后文还会提到线程化版本
一个思考,为什么在这个案例中,非阻塞 IO 比 fork 版本要快?是因为线程切换的成本吗?
非阻塞 connect

本文信息 | 本文信息 | 防爬虫替换信息 |
---|---|---|
作者网站 | LYMTICS | https://lymtics.top |
作者 | LYMTICS(樵仙) | https://lymtics.top |
联系方式 | me@tencent.ml | me@tencent.ml |
原文标题 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 |
原文地址 | https://www.cnblogs.com/lymtics/p/16364065.html | https://www.cnblogs.com/lymtics/p/16364065.html |
- 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
- 原文会不断地更新和完善,排版和样式会更加适合阅读,并且有相关配图
- 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息

三个用途:
- 完成一个 connect 需要至少一个 RTT,这个时间可以做别的事情
- 可以使用这个技术同时建立多个连接,如 Web 浏览器
- 给 connect 设置超时(其中部分方法在此文中已经提到过了)
两个细节:
-
尽管套接字是非阻塞的,但是如果连接到的服务器在同一个主机上,连接通常立即建立
-
源自 Berkeley 的实现有关于 select 和非阻塞 connect 的一下两个规则::
- 当连接建立成功,描述符变为可写
- 当连接建立遇到错误时,描述符变为可读可写
时间获取客户程序
代码
首先是修改时间获取程序的发起代码:
if (connect_nonb(sockfd, (SA*) &servaddr, sizeof(servaddr), 0) < 0)
err_sys("connect error");
如下是 connect_nonb.c
:
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;
// 发起非阻塞 connect
if ((n = connect(sockfd, saptr, salen)) < 0)
// 忽略 E.IN.PROGRESS 错误
if (errno != EINPROGRESS)
return (-1);
/* Do whatever we want while the connect is taking place. */
// 处理直接就建立好了的情况
if (n == 0)
goto done; /* connect completed immediately */
// 用 select 建立对连接情况的监听
// 同时还有超时相关的处理操作
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
// 监听,等于 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);
// 由于有可能在调用 select 之前就已经成功建立了连接,套接字变得可读可写
// 这和建立连接错误时的行为是一致的,所以我们调用 getsockopt 并检查是否有待处理的错误来处理这种情况
// 这里存在兼容性问题,getsockopt 不同的实现上行为不太一致,只需知道这里进行错误判断就行了
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);
}
移植性问题
我们的关键问题是:怎样判断连接是否建立成功
因为:有可能在调用 select 之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写,这与套接口描述符出错时是一样的
上面的代码是如何解决的:使用 getsockopt 查看是否返回错误信息
但是:getsockopt 的返回值在不同的实现上是不同的
- 源自 Berkeley 的实现是返回 0 ,待处理的错误值存储在 errno 中
- 而源自 Solaris 的实现是返回 -1 ,待处理的错误存储在 errno 中
尽管如此,上面的代码逻辑可以处理这两种情况
一些可以替代 getsockopt 的其他的解决方法包括:
- 调用获取对端 socket 地址的 getpeername 代替 getsockopt 。如果调用 getpeername 失败,getpeername 返回 ENOTCONN ,表示连接建立失败,之后我们必须再以 SO_ERROR 调用 getsockopt 得到套接口描述符上的待处理错误
- 调用 read ,读取长度为 0 字节的数据。如果连接建立失败,则 read 会返回 -1 ,且相应的 errno 指明了连接失败的原因;如果连接建立成功,read 应该返回 0
- 再调用一次 connect 。它应该失败,如果错误 errno 是 EISCONN ,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的
非阻塞 connect 是网络编程中最不易移植的部分。避免这一问题的一个较简单的技术是为每一个连接创建一个线程。
判断被中断的 connect:
对于一个正常的阻塞式套接字,如果其上的 connect 调用在 TCP 连接完成前被终止,则它将返回 E.INTE,我们不能再调用 connect ,这样将返回 E.ADDR.IN.USE 错误。
这种情景下我们只能用像本章这样的方法,对该套接字是否连接成功进行判断。
个人备注
这里我们确实没有在 connect 上阻塞了,但是却是在 select 上阻塞(或者说在 connect_nonb 这个函数上阻塞了)
如果我们确实想做一些其他的事情,似乎只能对 connect_nonb 这个函数进行修改,这样不就有一些不实用了
Web客户程序
模型
假设一个网页中有若干张图片,则有若干种可能的加载方式:
- 一次只能发起一个请求,需要等数据读取完毕后再发起另一个
- 一次可以发送 N 个请求
对于阻塞式 connect,效率比较低,因此本章的代码将用非阻塞式 IO 进行改善
整体流程如下:
- 先读取一个首页
- 然后根据首页中的内容加载图片资源
(当然这里为了简化操作,只是模拟了这个过程)
代码
在所有的文件中通用的头文件 web.h
:
// 定义常量
#define MAXFILES 20 // 最大连接数量
#define SERV "80" /* port number or service name */
// 文件的结构
struct file {
char* f_name; // 对应命令行输入的文件路径
char* f_host; // 服务器所在的主机名或服务器地址
int f_fd; // 文件的套接字描述符
int f_flags; // 指定准备对文件进行的操作
} file[MAXFILES];
/// 定义操作常量
#define F_CONNECTING 1 /* connect() in progress */
#define F_READING 2 /* connect() complete; now reading */
#define F_DONE 4 /* all done */
#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"
/* globals */
int nconn, nfiles, nlefttoconn, nlefttoread, maxfd;
fd_set rset, wset;
/* function prototypes */
void home_page(const char*, const char*);
void start_connect(struct file*);
void write_get_cmd(struct file*);
主函数 web.c
:
int main(int argc, char** argv) {
int i, fd, n, maxnconn, flags, error;
char buf[MAXLINE];
fd_set rs, ws;
if (argc < 5)
err_quit("usage: web <#conns> <hostname> <homepage> <file1> ...");
// 用户设置的最大连接数
maxnconn = atoi(argv[1]);
// 以来自命令行参数的相关信息填写 file 数组
nfiles = min(argc - 4, MAXFILES);
for (i = 0; i < nfiles; i++) {
file[i].f_name = argv[i + 4];
file[i].f_host = argv[2];
file[i].f_flags = 0;
}
printf("nfiles = %d\n", nfiles);
// 加载首页
home_page(argv[2], argv[3]);
// 初始化描述符
FD_ZERO(&rset);
FD_ZERO(&wset);
// 最大描述符为 -1 ,表示还没开始
maxfd = -1;
// 剩余需要读取的数量 - 剩余需要连接的数量 - 文件的数量
nlefttoread = nlefttoconn = nfiles;
nconn = 0; // 连接数
// 循环部分
while (nlefttoread > 0) {
// 如果没有达到最大并行连接数,并且另有连接需要建立
while (nconn < maxnconn && nlefttoconn > 0) {
/* 4find a file to read */
for (i = 0; i < nfiles; i++)
if (file[i].f_flags == 0)
break;
if (i == nfiles)
err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
// 那就找到一个尚未处理的文件,然后调用 start_conn 发起另一个请求
start_connect(&file[i]);
// 活跃连接数 + 1
nconn++;
// 没有处理的文件数量 - 1
nlefttoconn--;
}
// select 监听事件
rs = rset;
ws = wset;
n = Select(maxfd + 1, &rs, &ws, NULL, NULL);
// 对于每一个文件
for (i = 0; i < nfiles; i++) {
flags = file[i].f_flags;
if (flags == 0 || flags & F_DONE)
continue;
fd = file[i].f_fd;
// 标志位为连接中,套接字可读可写
if (flags & F_CONNECTING &&
(FD_ISSET(fd, &rs) || FD_ISSET(fd, &ws))) {
n = sizeof(error);
// 使用了前文的方法判断套接字是否成功打开
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 ||
error != 0) {
err_ret("nonblocking connect failed for %s",
file[i].f_name);
}
/* 4connection established */
printf("connection established for %s\n", file[i].f_name);
FD_CLR(fd, &wset); /* no more writeability test */
write_get_cmd(&file[i]); /* write() the GET command */
// 标志位为读、描述符可读
} else if (flags & F_READING && FD_ISSET(fd, &rs)) {
// 读取
if ((n = Read(fd, buf, sizeof(buf))) == 0) {
printf("end-of-file on %s\n", file[i].f_name);
Close(fd);
file[i].f_flags = F_DONE; /* clears F_READING */
FD_CLR(fd, &rset);
nconn--;
nlefttoread--;
} else {
printf("read %d bytes from %s\n", n, file[i].f_name);
}
}
}
}
exit(0);
}
加载首页的代码 home_page.c
void home_page(const char* host, const char* fname) {
int fd, n;
char line[MAXLINE];
// 建立连接
fd = Tcp_connect(host, SERV); /* blocking connect() */
n = snprintf(line, sizeof(line), GET_CMD, fname);
Writen(fd, line, n);
for (;;) {
// 读取应答(不做任何处理)
if ((n = Read(fd, line, MAXLINE)) == 0)
break; /* server closed connection */
printf("read %d bytes of home page\n", n);
/* do whatever with data */
}
printf("end-of-file on home page\n");
// 关闭连接
Close(fd);
}
start_connect
:非阻塞的发起连接:
void start_connect(struct file* fptr) {
int fd, flags, n;
struct addrinfo* ai;
// 调用我们自己的 host_serv 查找并转换主机名和服务名
// 返回指向某个 addrinfo 结构数组的一个指针
ai = Host_serv(fptr->f_host, SERV, 0, SOCK_STREAM);
// 创建套接字
fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
fptr->f_fd = fd;
printf("start_connect for %s, fd %d\n", fptr->f_name, fd);
// 设置为非阻塞
flags = Fcntl(fd, F_GETFL, 0);
Fcntl(fd, F_SETFL, flags | O_NONBLOCK);
/* 4Initiate nonblocking connect to the server. */
// 发起非阻塞 connect
if ((n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0) {
if (errno != EINPROGRESS)
err_sys("nonblocking connect error");
// 设置文件状态
fptr->f_flags = F_CONNECTING;
// 设置 select
FD_SET(fd, &rset); /* select for reading and writing */
FD_SET(fd, &wset);
if (fd > maxfd)
maxfd = fd;
} else if (n >= 0) /* connect is already done */
// 连接直接建立好了,那就写数据
write_get_cmd(fptr); /* write() the GET command */
}
write_get_cmd
:写输出的相关代码:
void write_get_cmd(struct file* fptr) {
int n;
char line[MAXLINE];
n = snprintf(line, sizeof(line), GET_CMD, fptr->f_name);
Writen(fptr->f_fd, line, n);
printf("wrote %d bytes for %s\n", n, fptr->f_name);
// 设置状态为正在读取
fptr->f_flags = F_READING; /* clears F_CONNECTING */
// 设置标志位,表明已经准备好了提供输入
FD_SET(fptr->f_fd, &rset); /* will read server's reply */
if (fptr->f_fd > maxfd)
maxfd = fptr->f_fd;
}
总结
时序图如下:
性能问题:
- 同时连接数越多,理论上性能越好,但是性能的提升会越来越少
- 如果网络中存在拥塞,此时发送过多的请求会让网络更加拥堵,从而起到相反的效果
非阻塞 accept

本文信息 | 本文信息 | 防爬虫替换信息 |
---|---|---|
作者网站 | LYMTICS | https://lymtics.top |
作者 | LYMTICS(樵仙) | https://lymtics.top |
联系方式 | me@tencent.ml | me@tencent.ml |
原文标题 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 | 《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园 |
原文地址 | https://www.cnblogs.com/lymtics/p/16364065.html | https://www.cnblogs.com/lymtics/p/16364065.html |
- 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
- 原文会不断地更新和完善,排版和样式会更加适合阅读,并且有相关配图
- 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息

似乎不需要
回忆一下:
- accept 函数由 TCP server 调用,用于从已完成连接队列队头返回下一个已完成连接;如果该队列为空,则睡眠(阻塞)
- accept 非阻塞是指,调用时如果已完成连接队列为空,则不进行阻塞,直接返回
当有一个已经完成的连接准备好 accept 时,select 将作为可读描述符返回该连接的监听套接字。
因此,如果我们使用 select 在某个监听套接字上等待一个外来连接,那就没有必要把该监听套接字设置为非阻塞,这是因为如果 select 告诉我们该套接字上已经有连接就绪,那么随后的 accept 就不应该阻塞。
所以我们似乎不需要这个功能?
但是有问题
但是如果使用阻塞版本,可能会有一个定时问题:
如下代码,假如我们在关闭时发送一个 RST
int main(int argc, char** argv) {
int sockfd;
struct linger ling;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
// 当建立连接后,设置如下两个参数
// 由于这两个参数的设置,下面我们关闭的时候就会发送一个 RST
ling.l_onoff = 1; /* cause RST to be sent on close() */
ling.l_linger = 0;
Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
Close(sockfd);
exit(0);
}
而服务器又比较繁忙,它无法在 select 返回监听套接字的可读条件后就立马调用 accept
当客户在服务器调用 accept 之前终止某个连接时,源自 Berkeley 的实现不把这个终止的连接返回给服务器,而其他实现应该返回 E.CONN.ABORTED
错误,却往往代之以返回 E.PROTO
错误;
考虑在前面一种情况下的这个过程:
- 客户建立一个连接,然后终止它
- select 向服务器进程返回可读,服务器过了若干时间才调用 accept
- 在服务器从 select 返回到调用 accept 期间,服务器 TCP 收到了客户的 RST
- 这个已完成的连接被服务器 TCP 驱逐出队列,我们假设此时队列中没有其他已完成的连接
- 服务器调用 accept ,但是由于没有任何已完成的连接,服务器阻塞
- 服务器一直阻塞在 accept 调用上,无法处理其他已经就绪的描述符,直到有新的连接建立起来
解决办法
- 当使用 select 获悉某个监听套接字上何时有已经完成的连接准备好被 accept 时,总是把这个监听套接字设置为非阻塞
- 在后续的 accept 调用中忽略如下表格中的错误
错误 | 实现来源 | 发生时机 |
---|---|---|
E.WOULD.BLOCK | Berkeley | 客户终止连接时 |
E.CONN.ABORTED | POSIX | 客户终止连接时 |
E.PROTO | SVR4 | 客户终止连接时 |
E.INTE | 如果有信号被捕获 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现