一、阻塞IO

1> 最常用、最常见的,效率最低的操作

2> 当IO事件没有得到满足时,IO相关函数不会返回,只有当IO事件满足后,对应函数才会解除阻塞

3> 目前接触的阻塞函数:scanf、getchar、read、write、recv、accept

二、非阻塞IO

1> 防止进程在IO操作上进行阻塞,如果对应缓冲区中没有数据,想使程序直接执行后续操作,而不再IO操作上阻塞

2> fcntl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
       #include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
功能:获取或设置文件描述符的属性
参数1:要设置的文件描述符
参数2:指令,根据不同的指令,该函数执行不同功能
F_GETFL:获取文件描述符的属性,如果cmd为该值,则第三个参数可以忽略
F_SETFL:设置文件描述符的属性,如果cmd为该值,则第三个参数为int类型
参数3:根据参数2而定,如果cmd为该值,则第三个参数为int类型表示要设置文件描述符的状态
O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and O_NONBLOCK(非阻塞属性)
返回值:如果参数为F_SETFL,成功返回当前文件描述符的状态,失败返回-1并置位错误码
如果参数为F_GETFL,成功返回0,失败返回-1并置位错误码

使用方法:
int arg = fcntl(0, F_GETFL); //获取文件描述符之前的状态
arg = arg|O_NONBLOCK; //加上其他属性
fcntl(0, F_SETFL, arg); //将改变后的属性设置回去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<myhead.h>

int main(int argc, const char *argv[])
{

int num; //定义一个整形变量

int arg = fcntl(0, F_GETFL); //获取文件描述符之前的状态
arg |= O_NONBLOCK; //加上其他属性
fcntl(0, F_SETFL, arg); //将改变后的属性设置回去

while(1)
{
int res = scanf("%d", &num);
printf("res = %d, num = %d\n", res, num);
}


return 0;
}

三、IO多路复用

1> 对于多个阻塞任务想要并发执行时,对于有操作系统的内核上,可以选择多进程或多线程来完成

2> 如果没有操作系统,实现多任务同时执行就需要使用其他技术了:IO多路复用

3> 无需创建新的进程或者线程,能够减少系统资源的开销,减少上下文切换的次数

4> 原理图

img

5> 实现IO多路复用的函数:select、poll、epoll(驱动课程学习)

3.1 select

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
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:阻塞函数,检测管理的文件描述符集合,直到该集合中有一个或多个文件描述符事件发生,并解除阻塞
参数1:要检测的文件描述符集合中的最大文件描述符 + 1
参数234:分别表示读文件描述符集合、写文件描述符集合、异常文件描述符集合,由于写操作也可以转换为读操作,所以,对于小的程序而言,只需要一个集合即可
,不使用的集合填NULL即可
参数5:超时时间,给定一个阻塞时间,如果时间到了,就自动解除select的阻塞,NULL表示永久阻塞
struct timeval {
long tv_sec; /* seconds */ 秒数
long tv_usec; /* microseconds */ 微秒
};
返回值:>0:本次成功触发解除阻塞的文件描述符个数,当解除select阻塞室,文件描述符集合中只剩下本次触发解除阻塞的文件描述符了
=0:表示没有任何事件产生,但是超时
=-1:失败返回-1并置位错误码


有关集合的操作函数:

void FD_CLR(int fd, fd_set *set); //将文件描述符fd从集合set中清除
int FD_ISSET(int fd, fd_set *set); //判断文件描述符fd是否存在于集合set中
void FD_SET(int fd, fd_set *set); //将文件描述符fd放入集合set中
void FD_ZERO(fd_set *set); //将文件描述符集合清空

select模型实现

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.122.118" //服务器IP地址


int main(int argc, const char *argv[])
{
//1、创建用于连接的套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:通信域,表明使用的是ipv4协议
//参数2:通信方式,使用TCP通信
//参数3:0表示之前已经指定协议 IPPROTO_TCP

if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd); //3


//将端口号快速重用函数
int reuse = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");



//2、给当前套接字绑定IP地址和端口号
//2.1填充要绑定的地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址

//2.2 绑定
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success %s %s %d\n", __FILE__, __func__, __LINE__);

//3、将套接字设置成监听状态
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success %s %s %d\n", __FILE__, __func__, __LINE__);

//4、阻塞等待客户端的链接请求
//4.1定义容器接收客户端的地址信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t socklen = sizeof(cin); //用于接收地址信息的大小

int newfd = -1;

//11、准备一个文件描述符容器
fd_set readfds, tempfds;
//22、清空容器
FD_ZERO(&readfds);
//33、将要检测的文件描述符放入集合
FD_SET(0, &readfds);
FD_SET(sfd, &readfds);


while(1)
{
tempfds = readfds; //存储一份数据

//使用select函数对容器中的文件描述符进行检测
int res = select(sfd+1, &tempfds, NULL, NULL, NULL);
if(res == -1)
{
perror("select error");
return -1;
}else if(res == 0)
{
printf("timeout\n");
return -1;
}

//程序执行至此,说明已经有事件产生并且解除了select的阻塞
//并且此时文件描述符集合中只剩下本次触发的事件对应的文件描述符
//判断哪个文件描述符还在集合中,如果在,就执行相关函数
if(FD_ISSET(sfd, &tempfds))
{

//4.2 接收客户端的链接
newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]发来链接请求 %s %s %d\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),__FILE__, __func__, __LINE__);

}

//判断0号文件描述符是否在集合中
if(FD_ISSET(0, &tempfds))
{

//终端输入
char wbuf[128] = "";
scanf("%s", wbuf);
printf("触发了终端输入事件。。。\n");
}


/*5、跟客户端进行消息通信
char buf[128] = "";
while(1)
{
//将数组清空
bzero(buf, sizeof(buf));

//读取客户端发来的消息
//int res = read(newfd, buf, sizeof(buf));
int res = recv(newfd, buf, sizeof(buf), 0);
if(res == 0)
{
printf("客户端已经下线\n");
break;
}
printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);

//给客户端发消息
strcat(buf, "*_*");

//write(newfd, buf, sizeof(buf));
send(newfd, buf, sizeof(buf), 0);
printf("发送成功\n");
}

//关闭当前通信的套接字
close(newfd);
*/
}

//6、关闭套接字
close(sfd);





return 0;
}

3.2 使用IO多路复用完成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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.122.118" //服务器IP地址


int main(int argc, const char *argv[])
{
//1、创建用于连接的套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:通信域,表明使用的是ipv4协议
//参数2:通信方式,使用TCP通信
//参数3:0表示之前已经指定协议 IPPROTO_TCP

if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd); //3


//将端口号快速重用函数
int reuse = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");



//2、给当前套接字绑定IP地址和端口号
//2.1填充要绑定的地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址

//2.2 绑定
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success %s %s %d\n", __FILE__, __func__, __LINE__);

//3、将套接字设置成监听状态
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success %s %s %d\n", __FILE__, __func__, __LINE__);

//4、阻塞等待客户端的链接请求
//4.1定义容器接收客户端的地址信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t socklen = sizeof(cin); //用于接收地址信息的大小

int newfd = -1;

//11、准备一个文件描述符容器
fd_set readfds, tempfds;
//22、清空容器
FD_ZERO(&readfds);
//33、将要检测的文件描述符放入集合
FD_SET(0, &readfds);
FD_SET(sfd, &readfds);

//定义一个变量存储容器中最大的文件描述符
int maxfd = sfd;

//定义一个地址信息结构体数组,每一个元素对应一个客户端文件描述符
struct sockaddr_in cin_arr[1024];



while(1)
{
tempfds = readfds; //存储一份数据

//使用select函数对容器中的文件描述符进行检测
int res = select(maxfd+1, &tempfds, NULL, NULL, NULL);
if(res == -1)
{
perror("select error");
return -1;
}else if(res == 0)
{
printf("timeout\n");
return -1;
}


//判断是否是文件描述符触发事件
for(int cli=0; cli<=maxfd; cli++)
{
//当遍历到不存在的文件描述符直接pass
if(!FD_ISSET(cli, &tempfds))
{
continue;
}

//程序执行至此,说明已经有事件产生并且解除了select的阻塞
//并且此时文件描述符集合中只剩下本次触发的事件对应的文件描述符
//判断哪个文件描述符还在集合中,如果在,就执行相关函数
if(cli == sfd)
{

//4.2 接收客户端的链接
newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]发来链接请求 %s %s %d\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),__FILE__, __func__, __LINE__);

//更新地址信息结构体数组
cin_arr[newfd] = cin;

//将newfd放入readfds中
FD_SET(newfd, &readfds);

//更新maxfd
if(newfd > maxfd)
{
maxfd = newfd;
}

}else
//判断0号文件描述符是否在集合中
if(cli == 0)
{

//终端输入
char wbuf[128] = "";
scanf("%s", wbuf);
printf("触发了终端输入事件。。。\n");
if(strcmp(wbuf, "quit") == 0)
{
goto END;
}

//将消息发送给所有客户端
for(int i=4; i<=maxfd; i++ )
{
sendto(i, wbuf, sizeof(wbuf), 0,(struct sockaddr*) &cin_arr[i], sizeof(cin_arr[i]));
}

}else
{


//5、跟客户端进行消息通信
char buf[128] = "";

//将数组清空
bzero(buf, sizeof(buf));

//读取客户端发来的消息
//int res = read(newfd, buf, sizeof(buf));
int res = recv(cli, buf, sizeof(buf), 0);
if(res == 0)
{
printf("客户端已经下线\n");
//关闭当前通信的套接字
close(cli);
//将当前文件描述符从文件描述符集合中移除
FD_CLR(cli, &readfds);

//更新maxfd
for(int i=maxfd; i>=sfd; i--)
{
if(FD_ISSET(i, &readfds))
{
maxfd = i;
break;
}
}



continue;
}
printf("[%s:%d] : %s\n", inet_ntoa(cin_arr[cli].sin_addr), ntohs(cin_arr[cli].sin_port), buf);

//给客户端发消息
strcat(buf, "*_*");

//write(newfd, buf, sizeof(buf));
send(cli, buf, sizeof(buf), 0);
printf("发送成功\n");


}
}
}

END:
//6、关闭套接字
close(sfd);





return 0;
}

3.3 poll实现多路复用

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
       #include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:阻塞等待文件描述符结构体数组中是否有事件产生,如果有事件产生,则解除阻塞
参数1:文件描述符结构体数组
struct pollfd {
int fd; /* file descriptor */ 要检测的文件描述符
short events; /* requested events */ 要等待的事件
short revents; /* returned events */ 触发的事件,用于返回结果
};
关于events和revents的值:
POLLIN:读事件
POLLOUT:写事件
POLLERR:错误,只在revents中收到,不能设置到events中



参数2:参数1数组的长度,也就是检测文件描述符的个数
参数3:超时时间,以毫秒为单位
>0:表示设置的超时时间
=0:非阻塞
=-1:表示阻塞
返回值:成功返回触发本次接触阻塞的文件描述符的个数
0:表示超时
-1:表示失败,并置位错误码

1> poll实现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
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.122.118" //服务器ip地址
#define CLI_PORT 6666 //客户端的端口号
#define CLI_IP "192.168.122.118" //客户端ip地址


int main(int argc, const char *argv[])
{
//1、创建用于通信的套接字文件描述符
int cfd = -1;
cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("cfd = %d\n", cfd); //3

//2、绑定(可选)
//2.1 填充地址信息结构体
struct sockaddr_in cin;
cin.sin_family = AF_INET;
cin.sin_port = htons(CLI_PORT);
cin.sin_addr.s_addr = inet_addr(CLI_IP);
//2.2绑定
if(bind(cfd, (struct sockaddr*)&cin, sizeof(cin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

//3、连接服务器
//3.1 填充服务器地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT);
sin.sin_addr.s_addr = inet_addr(SER_IP);
//3.2 连接
if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("connect error");
return -1;
}
printf("connect success\n");

//11、定义一个等待文件描述符结构体数组
struct pollfd pfd[2];

//22、填充要等待的文件描述符及事件
pfd[0].fd = 0; //将0号文件描述符放入检测集合中
pfd[0].events = POLLIN; //表示检测该文件描述符的读事件

pfd[1].fd = cfd; //将cfd文件描述符放入检测集合中
pfd[1].events = POLLIN; //表示检测该文件描述符的读事件


//4、收发数据
char buf[128] = "";
while(1)
{

//阻塞检测集合中是否有事件产生
int res = poll(pfd, 2, -1); //-1表示一直阻塞
if(res == -1)
{
perror("poll error");
return -1;
}else if(res == 0)
{
printf("timeout\n");
return -1;
}

//程序执行至此,表示已经有事件产生

//判断是0号文件描述符产生的事件
if(pfd[0].revents == POLLIN)
{


//清空数组
bzero(buf, sizeof(buf));

//printf("请输入>>>");
fgets(buf, sizeof(buf), stdin); //从终端输入数据
buf[strlen(buf)-1] = 0; //将换行改为'\0'

//发送给服务器
send(cfd, buf, sizeof(buf), 0);
printf("发送成功\n");
if(strcmp(buf, "quit") == 0)
{
break;
}
}

//判断是cfd产生的事件
if(pfd[1].revents == POLLIN)
{

//接收服务器发来的消息
recv(cfd, buf, sizeof(buf), 0);
printf("[%s:%d]:%s\n", SER_IP, SER_PORT, buf);
}
}

//5、关闭套接字
close(cfd);

return 0;
}

作业

1> 使用select实现TCP客户端的并发

2> 使用poll实现TCP服务器的并发