Unix Network Programming - 3. 非阻塞I/O

非阻塞I/O

非阻塞I/O总是会和I/O复用一起使用.
why?

  1. 非阻塞I/O轮询浪费CPU;
  2. I/O复用和阻塞I/O搭配更不合适, 套接字函数会阻塞.
  3. 非阻塞IO的核心思想是避免阻塞发生在read和write或其他IO系统调用上, 尽可能的使线程服务多个socket.
  4. IO线程只会阻塞在IO复用时.

非阻塞I/O总是需要在应用层建立缓冲区.
why?

  1. 数据发送时, 如果内核缓冲区不够大, 多余的数据将会阻塞发送, 所以在应用层缓存多余的数据避免阻塞.
  2. 数据读取时, 如果从内核中读取的数据并不完整, 那么需要缓存这部分数据, 等剩下的数据全部收到一并处理.

如何设计缓冲区?

  1. 尽可能减少系统调用.
  2. 尽可能减少内存使用. 使用vector, 自适应大小.

需要断开连接时, 如何保证应用缓冲区中的数据能够完整到达或发送?

  1. TCP连接不能立即断开, 需要等待数据发送完成才能断开.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include "Common.h"

void str_cli(FILE* fd, int sockfd) {
// 使用fcntl把描述符设置为非阻塞
int flag = 0;
flag = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flag | O_NONBLOCK); // socket
flag = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, flag | O_NONBLOCK); // 标准输入
flag = fcntl(STDOUT_FILENO, F_GETFL, 0); fcntl(STDOUT_FILENO,F_SETFL, flag | O_NONBLOCK); // 标准输出

// 初始化缓冲区及缓冲区指针
char to[MAXLINE], from[MAXLINE];
char* pti, * pto, * pfi, * pfo;
pti = pto = to;
pfi = pfo = from;

int maxfd = std::max(std::max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;

fd_set rset, wset;

int stdineof = 0;

// 主循环
while (1) {
FD_ZERO(&rset);
FD_ZERO(&wset);

// 根据缓冲区状态设置描述符的读写状态
if (stdineof == 0 && pti < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset); // 客户端发送缓冲区任然接收数据, 且缓冲区有空闲空间, 则可以从标准输入中读取数据
if (pfi < &from[MAXLINE]) FD_SET(sockfd, &rset); // 客户端接收缓冲区有空闲空间, 则可以从socket中读取数据
if (pti < pto) FD_SET(sockfd, &wset); // 客户端发送缓冲区有数据, 则可将其写入socket
if (pfi < pfo) FD_SET(STDOUT_FILENO, &wset); // 客户端接收缓冲区有数据, 则将写入标准输出

select(maxfd, &rset, &wset, 0, 0);

if (FD_ISSET(STDIN_FILENO, &rset)) {
int n = read(STDIN_FILENO, pti, &to[MAXLINE] - pti);

if (n == 0) {
if (errno != EWOULDBLOCK) std::cout << "read stdin EWOULDBLOCK???." << std::endl;

std::cout << "EOF in stdin" << std::endl;
stdineof = 1;
if (pto == pti) shutdown(sockfd, SHUT_WR); // 客户端断开连接, 发送缓冲区没有数据需要发送, 向服务器发送FIN
}
else {
std::cout << "stdin read " << n << " bytes." << std::endl;

pti += n; // 修改发送缓冲区, 此时缓冲区有数据了, sockfd 可写
FD_SET(sockfd, &wset);
}
}

if (FD_ISSET(sockfd, &rset)) {
int n = read(sockfd, pfo, &from[MAXLINE] - pfo);
if (n == 0) {
if (errno != EWOULDBLOCK) std::cout << "read socket EWOULDBLOCK???." << std::endl;

std::cout << "EFO in socket" << std::endl;

if (stdineof == 1) return;
else std::cout << "server terminated prematurely" << std::endl; // 收到服务器断开连接
}
else {
std::cout << "socket read " << n << " bytes." << std::endl;

pfi += n; // 修改缓冲区, 此时接收缓冲区有数据了, 标准输出可写
FD_SET(STDOUT_FILENO, &wset);
}
}

if (FD_ISSET(sockfd, &wset)) {
int n = pti - pto;
if (n > 0) {
int len = write(sockfd, pto, n);
if (len == 0) {
if (errno != EWOULDBLOCK) std::cout << "write socket EWOULDBLOCK???." << std::endl;
else std::cout << "socket write error" << std::endl;
}
else {
std::cout << "socket write " << len << " bytes." << std::endl;

pto += n;
if (pto == pti){
pto = pti = to;
if (stdineof) shutdown(sockfd, SHUT_WR); // 数据发送完了, 客户端断开连接, 向服务器发送FIN
}
}
}
}

// write 到 socket
if (FD_ISSET(STDOUT_FILENO, &wset)) {
int n = pfi - pfo;
if (n > 0) {
int len = write(STDOUT_FILENO, pfo, n);
if (len == 0) {
if (errno != EWOULDBLOCK) std::cout << "write stdout EWOULDBLOCK???." << std::endl;
else std::cout << "stdout write error" << std::endl;
}
else {
std::cout << "stdout write " << len << " bytes." << std::endl;

pfo += n;
if (pfi == pfo) pfi = pfo = from; // 接收缓冲区读空了, 重置它
}
}
}
}
}


费这么大劲做非阻塞版本"得不偿失"了, 当需要使用非阻塞I/O时, 更简单的方法是将任务划分到多个进程或线程

## 非阻塞connect

完成一个connect要一个RTT时间, RTT波动很大, 所以将其设置为非阻塞很有必要
1. 需要同时建立连接
2. 使用select等待建立连接, 通过select指定一个时限

尽管scoket是非阻塞的, 如果连接到的服务器在同一个主机上, 但调用connect时, 连接通常立刻建立, 必须处理这种情况
select和非阻塞connect的规定, 当连接成功建立时, 描述符变为可写, 当连接建立遇到错误时, 描述符变为可读又可写??

## 非阻塞accept