Linux网络编程【9】(IO模型及多路复用) 1 IO模型

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1 IO模型

1.1 分类

在UNIX/Linux下主要有4种I/O 模型:

  1. 阻塞I/O:最常用、最简单、效率最低
  2. 非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
  3. I/O 多路复用:允许同时对多个I/O进行控制
  4. 信号驱动I/O:一种异步通信模型

1.2 阻塞IO

几乎所有的阻塞函数默认都是阻塞IO,

以读阻塞为例,

如果要读取的缓冲区中有数据,则正常执行

如果缓冲区中没有数据,则读函数会一直阻塞等待,当有数据的时候,==内核将会自动唤醒当前进程==,接着执行读操作

以写阻塞为例,

一般写操作是不会阻塞,只有当写操作对应的缓冲区写满时,会发生阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
char buf[32] = {0};

while(1)
{
fgets(buf, 32, stdin);
//标准输入都有文件描述符0,1,2
//没有这三个也可以操作,终端是设备文件

sleep(1);

printf("***************************\n");
}

return 0;
}

1.3 非阻塞IO

如果将一个函数设置为非阻塞,意味着:

  • 如果要操作的缓冲区中有数据,则正常执行
  • 如果要操作的缓冲区中没有数据,则当前函数立即返回(当前函数执行失败),接着执行下面的代码

当一个应用程序使用了非阻塞模式套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称作polling),当应用程序不停地polling内核来检查是否I/O操作已经就绪,这是非常浪费CPU的资源的。

有一部分函数自带标志位,可以设置非阻塞,但是大多数函数都无法直接设置非阻塞,需要通过一些函数来设置

使用fcntl函数设置非阻塞IO

  • WNOHANG
  • MSG_DONTWAIT
  • O_NONBLOCK
  1. 头文件:
    1. #include <unistd.h>
    2. #include <fcntl.h>
  2. 原型:int fcntl(int fd, int cmd, ... /* arg */ );
  3. 功能:操作一个文件描述符
  4. 参数:
    1. fd:文件描述符
    2. cmd:命令选项
      1. F_GETFL 获取文件状态标志位
      2. F_SETFL 设置文件状态标志位
      3. O_NONBLOCK 非阻塞
    3. …arg:可变参,是否需要由cmd后面括号里面内容决定,如果是int就需要,如果是void就不需要
  5. 返回值:
    1. 成功:
      F_GETFL 文件状态标志位
      F_SETFL 0
    2. 失败:-1

读改写:一位一位的改

  • 第一步:读,获取之前标志位
  • 第二步:改,改变标志位
  • 第三步:写,将改后的标志位设置回去
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
c复制代码#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
char buf[32] = {0};

//使用fcntl函数实现非阻塞
//注意:对于标志位的操作,必须遵循只操作指定标志位而不改变其他标志位
//所以对寄存器或者位的操作,一般执行读改写三步

//第一步:读,获取之前标志位
int flags;
if((flags = fcntl(0, F_GETFL)) == -1)
{
perror("fcntl error");
exit(1);
}

//第二步:改,改变标志位
flags |= O_NONBLOCK;

//第三步:写,将改后的标志位设置回去
if(fcntl(0, F_SETFL, flags) == -1)
{
perror("fcntl error");
exit(1);
}

while(1)
{
if(fgets(buf, 32, stdin) == NULL)
{
perror("fgets error");
}

sleep(1);

printf("buf = [%s]\n", buf);
printf("***************************\n");
}

return 0;
}

2 IO多路复用

❓当一个代码中有多个阻塞函数时,因为代码默认都有先后执行顺序,所以无法做到每一个阻塞函数独立执行,相互没有影响,如何解决这个问题?

  • 如果按照默认阻塞形式,无法解决;
  • 如果设置为非阻塞,每一个函数都轮询查看缓冲区中是否有数据,可以解决这个问题,但是轮询比较消耗CPU资源,所以也不推荐;
  • 如果使用多进程或者多线程,需要考虑资源释放问题,也不推荐。

与多线程、多进程相比,I/O多路复用系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

  • 处理多个描述符
  • 服务器要处理多个服务或者多个协议

🀄相对比较好的方法是使用IO多路复用

IO多路复用的基本思想是:

  1. 先构造一张有关描述符的表,保存要操作的文件描述符;
  2. 然后调用一个函数,阻塞等待文件描述符准备就绪;
  3. 当有文件描述符准备就绪;
  4. 则函数立即返回;
  5. 执行相应的IO操作。

图片.png

调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。

2.1使用select实现IO多路复用

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
c复制代码#include <sys/select.h>
#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);
功能:允许一个程序操作多个文件描述符,阻塞等待文件描述符
准备就绪,如果有文件描述符准备就绪,函数立即返回,
执行相应的IO操作
参数:
nfds:最大的文件描述符加1
readfds:保存读操作文件描述符的集合
writefds:保存写操作文件描述符的集合
exceptfds:保存其他或者异常的文件描述符的集合
timeout:超时
NULL 阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1

清空集合set
void FD_ZERO(fd_set *set);

将文件描述符fd添加到集合set中
void FD_SET(int fd, fd_set *set);

将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);

判断文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:
存在:1
不存在:0

2.1.2 服务器

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
c复制代码//TCP网络编程之服务器

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)

int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}

int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};

//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}

//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));

//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}

//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}

//使用select实现IO多路服用

//第一步:创建一个保存要操作的文件描述符集合并清空
fd_set readfds;
FD_ZERO(&readfds);

int maxfd = sockfd;

while(1)
{
//第二步:将要操作的文件描述符添加到集合中
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);

//第三步:调用select函数,阻塞等待文件描述符准备就绪
if(select(maxfd+1, &readfds, NULL, NULL, NULL) == -1)
{
ERRLOG("select error");
}

//第四步:如果有文件描述符准备就绪,则select函数立即返回执行对应的IO操作
//注意:如果有文件描述符准备就绪,select函数返回之后,会自动将集合中没有准备就绪的文件描述符移除
//所以select函数返回之后,判断哪个文件描述符还在集合中,在的就是准备就绪的

if(FD_ISSET(0, &readfds) == 1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
}

if(FD_ISSET(sockfd, &readfds) == 1)
{
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
}

return 0;
}

2.1.3客户端

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
c复制代码//TCP网络编程之客户端

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>

#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)

int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}

int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);

//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}

//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));

//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}

//进行通信
char buf[N] = {0};
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';

if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}

if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}

memset(buf, 0, N);

if(recv(sockfd, buf, N, 0) == -1)
{
ERRLOG("recv error");
}

printf("服务器:%s\n", buf);
}

return 0;
}

2.2poll

1.6.1 poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:同select,阻塞等待文件描述符准备就绪,如果有文件描述符
准备就绪,则函数立即返回并执行相应的IO操作
参数:
fds:结构体数组,有多少个元素由要操作的文件描述符的个数决定
struct pollfd {
int fd; 文件描述符
short events; 请求的事件
POLLIN 有数据可读
short revents; 返回的事件
};
nfds:要操作的文件描述符的个数
timeout:超时检测
>0 设置超时毫秒数
0 非阻塞
<0 阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%