非阻塞式 I/O
非阻塞式 I/O
概述
非阻塞式 I/O, 相对复杂, 增加了很繁琐的输入输出缓冲区, 通常讲解此类 I/O 会用下面这张图来描述, 对于非阻塞式的涉及来说, 光这一张图的讲解太过敷衍.
先大致描述以下这张图, 进程调用 recvfrom 方法, 向内核获取 I/O 数据(也就是输入输出, 缓冲流的数据), 如果内核有数据, 则复制数据并返回结果, 如果没有, 则返回 BWOULDBLOCK 标志(不同系统标志不尽相同). 仅单纯的使用非阻塞式系统调用, 性能不会太好, 需要有很多复杂的缓冲流维护.
相较于阻塞式 I/O
了解非阻塞式 I/O, 必然要对 阻塞式 I/O 的痛点有一定的了解.
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
// 标识符, 标志是否处理客户输入
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0) // 当且仅当标识符为默认状态下, 才服务客户端输入的数据
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket 可读 */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1) return; /* 正常终端 */
else err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* 标准输入可读(用户开始输入数据) */
// 处理完用户的所有数据, 将 stdineof 标志符**, 此时不再处理客户数据
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* 发送 FIN 标志, 终止对套接字的写操作 */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
-
当标准输入可读, 本进程便向标准输入索要数据, 并将得到的数据通过 writen 写入 sockfd 发送缓冲区(buf)中, 但是, 如果 sockfd 发送缓冲区已经被写满(网络过慢, 还来不及发送给服务器), 此时, 进程阻塞于 writen 操作.
-
当套接字缓冲区有数据可读, sockfd 可读, 进程将获取到的数据通过 write 写到准输出, 但是如果 write 操作的速度甚至慢于网络传输, 那么进程将阻塞于写操作, 无法顾及服务端新发送的数据.
由于以上两点原因, 我们可以将 io 操作拆的更细一点, 这里将从标准输入读, 写入套接字, 套接字读, 套接字写入标准输入四部分全部拆出来, 只要有任意一部分 I/O 就绪, 则执行. 为了监听套接字缓冲区是否占满或者是否为空, 我们需要在添加两个缓冲区分别缓冲套接字发送缓冲区和套接字接收缓冲区.
从标准输入到服务器的数据
从套接字到标准输出的数据
非阻塞式 I/O 时间线
这里需要使用非阻塞式 I/O, 即尝试读写, 失败则跳过继续, 防止进程阻塞.
#include "unp.h"
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;
// 监听套接字, 并设置为非阻塞
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; /* 初始化 fr, to 两个缓冲区 */
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for ( ; ; ) {
FD_ZERO(&rset);
FD_ZERO(&wset);
// 当输入缓冲区(to)有空闲位置可以存放标准输入的数据, 则监听 STDIN_FILENO
if (stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset); /* 从标准输入开始读 */
// 当套接字缓冲区(fr)有空闲位置可以接收 socket 数据, 监听套接字的读操作
if (friptr < &fr[MAXLINE])
FD_SET(sockfd, &rset); /* 读 socket */
// 当存在需要写入套接字的数据, 开始监听套接字的写操作
if (tooptr != toiptr)
FD_SET(sockfd, &wset); /* 写入 socket */
// 当发往标准输出区域有数据, 则开始监听标准输出的写操作
if (froptr != friptr)
FD_SET(STDOUT_FILENO, &wset); /* 写入数据到标准输出 */
// 此时, 根据上述条件开始选择性的监听套接字的读和写操作.
Select(maxfdp1, &rset, &wset, NULL, NULL);
// 当输入缓冲区(to)有空闲位置可以存放标准输入的数据, 则监听 STDIN_FILENO
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) {
#ifdef VOL2
fprintf(stderr, "%s: EOF on stdin\n", gf_time());
#endif
stdineof = 1; /* all done with stdin */
if (tooptr == toiptr)
Shutdown(sockfd, SHUT_WR);/* send FIN */
} else {
#ifdef VOL2
fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);
#endif
toiptr += n; /* # just read */
FD_SET(sockfd, &wset); /* try and write to socket below */
}
}
// 当套接字缓冲区(fr)有空闲位置可以接收 socket 数据, 监听套接字的读操作
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) {
#ifdef VOL2
fprintf(stderr, "%s: EOF on socket\n", gf_time());
#endif
if (stdineof)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
} else {
#ifdef VOL2
fprintf(stderr, "%s: read %d bytes from socket\n",
gf_time(), n);
#endif
friptr += n; /* # just read */
FD_SET(STDOUT_FILENO, &wset); /* try and write below */
}
}
// 当存在需要写入套接字的数据, 开始监听套接字的写操作
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 {
#ifdef VOL2
fprintf(stderr, "%s: wrote %d bytes to stdout\n",
gf_time(), nwritten);
#endif
froptr += nwritten; /* # just written */
if (froptr == friptr)
froptr = friptr = fr; /* back to beginning of buffer */
}
}
// 当发往标准输出区域有数据, 则开始监听标准输出的写操作
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 {
#ifdef VOL2
fprintf(stderr, "%s: wrote %d bytes to socket\n",
gf_time(), nwritten);
#endif
tooptr += nwritten; /* # just written */
if (tooptr == toiptr) {
toiptr = tooptr = to; /* back to beginning of buffer */
if (stdineof)
Shutdown(sockfd, SHUT_WR); /* send FIN */
}
}
}
}
}
以上便实现了单进程的 I/O 复用且非阻塞式的套接字编程.