非阻塞式 IO
套接字默认是阻塞的,分为以下4类:
- 输入操作:read、readv、recv、recvfrom、fecvmsg,对于面向流的TCP可使用自定义的readn函数或
MSG_WAITALL
标志指定等到某个固定数目的数据可读为止,没有数据可读时非阻塞IO立即返回一个EWOULDBLOCK
错误 - 输出操作:write、writev、send、sendto、sendmsg,将应用进程缓冲区数据复制到内核缓冲区,对于TCP没有足够空间写数据时非阻塞输出函数立即返回一个 EWOULDBLOCK,UDP没有真正的内核发送缓冲区但可能因为其他原因阻塞
- 接收外来连接 accept,无连接时立即返回 EWOULDBLOCK
- 发起外出连接,即TCP的 connect 函数。调用非阻塞 connect 后在三次握手完成前返回一个 EINPROGRESS 错误,当客户端与服务端位于同一机器时可能会直接返回成功
System V 返回 EAGAIN 错误;Berkeley 返回 EWOULDBLOCK 错误。POSIX 规定这两个都可以,多数提供将二者定义为相同的值
UNP中用 "非阻塞IO+select" 处理客户端socket
#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;
// 将套接字、输入输出IO设置为非阻塞
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;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for (;;) {
FD_ZERO(&rset);
FD_ZERO(&wset);
// 输出缓冲区还有空间用于读取标准输入,且输入数据没读完
if (stdineof == 0 && toiptr < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset);
// 输入缓冲区还有空间用于读取套接字
if (friptr < &fr[MAXLINE]) FD_SET(sockfd, &rset);
// 有数据要发给服务器
if (tooptr != toiptr) FD_SET(sockfd, &wset);
// 有数据要输出到标准输出
if (froptr != friptr) FD_SET(STDOUT_FILENO, &wset);
Select(maxfdp1, &rset, &wset, NULL, NULL);
// 从 stdin 读取
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) { // 读取到 EOF
stdineof = 1;
if (tooptr == toiptr) Shutdown(sockfd, SHUT_WR);
} else {
toiptr += n;
FD_SET(sockfd, &wset);
}
}
// 从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) {
if (stdineof)
return;
else
err_quit("str_cli: server terminated prematurely");
} else {
friptr += n;
FD_SET(STDOUT_FILENO, &wset);
}
}
// 向 stdout 写
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;
if (froptr == friptr) froptr = friptr = fr;
}
}
// 向 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 {
tooptr += nwritten;
if (tooptr == toiptr) {
toiptr = tooptr = to;
if (stdineof) Shutdown(sockfd, SHUT_WR);
}
}
}
}
}
以上代码效率很高,但是代码太过复杂,可使用fork简化代码
使用 fork 实现双进程分别处理读写
#include "unp.h"
void str_cli(FILE *fp, int sockfd) {
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if ((pid = Fork()) == 0) {
while (Readline(sockfd, recvline, MAXLINE) > 0) Fputs(recvline, stdout);
kill(getppid(), SIGTERM); // 通知父进程不再有数据
exit(0);
}
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR); // 父进程只负责向socket写,所以此处只能通过 shtdown 关闭写端
pause(); // 等待子进程发送 SIGTERM 信号
return;
}
不同版本性能:
非阻塞 connect
非阻塞connect返回 EINPROGRESS 后可使用select检测这个连接成功或失败
- 节约时间
- 同时建立多个连接
- 可给select指定时间限制
注意:
- 如果双方在一台主机,调用connect时连接立即建立
- 在调用select前可能正确建立连接,套接字处于可读写状态
- 连接成功建立时,描述符可写;出错时描述符可读可写
判断select返回套接字是否成功:
- 使用
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0
检查套接字是否存在待处理错误 - 调用 getpeername,返回 ENOTCONN 表示失败返回,可通过 getsockopt 获取错误
- 以值未0的长度参数调用read,返回失败则connect失败,errno给出失败原因,如果连接成功建立则read返回0
- 再调用 connect 一次,如若返回 EISCONN 表示套接字已连接
一个阻塞的connect调用在等待三次握手时被中断,如捕获信号,这时应把它当作非阻塞IO调用select处理
非阻塞 accept
IO复用select 必须搭配非阻塞accept以防止“定时问题”
客户端使用connect建立连接后发送一个 RST 断开连接。
服务器的select调用返回,期间建立连接并加入队列,在接收RST后将连接移出队列。
使用accept接收连接时,因为连接已断开所以阻塞式accept会阻塞在这里,直到下一个客户端连接上来。
标签:FILENO,toiptr,friptr,阻塞,FD,IO,sockfd From: https://www.cnblogs.com/zhh567/p/17609068.html