JackCin's blog JackCin's blog
首页
  • 页面

    • Html
    • CSS
  • 核心

    • JavaScript基础
    • JavaScript高级
  • 框架

    • Vue
  • jQuery
  • Node
  • Ajax
Linux
  • 操作系统
  • 数据结构与算法
  • 51单片机
  • CC2530
  • 网站
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

JackCin

前端小菜鸡(✪ω✪)
首页
  • 页面

    • Html
    • CSS
  • 核心

    • JavaScript基础
    • JavaScript高级
  • 框架

    • Vue
  • jQuery
  • Node
  • Ajax
Linux
  • 操作系统
  • 数据结构与算法
  • 51单片机
  • CC2530
  • 网站
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Linux系统编程入门

  • Linux 多进程开发

  • Linux 多线程开发

  • Linux 网络编程

    • 网络基础
    • socket通信基础
    • IO多路复用
      • 1、BIO 和 NIO 模型
        • 1.1 BIO 模型
        • 1.2 NIO 模型
      • 2、I/O多路转接(复用)
      • 3、select()
        • 3.1 select 工作过程分析
        • 3.2 代码示例
        • 3.3 select 的缺点及与poll对比
      • 4、poll()
        • 4.1、poll函数
        • 4.2、改进
        • 4.2.1 改进一
        • 4.2.2 改进二
        • 4.3、思考
        • 4.3.1 思考一
        • 4.3.2 思考二
        • 4.3.2 思考三
      • 5、epoll()
        • 5.1 epoll 相关函数
        • 5.2 epoll基础案例
        • 5.3 补充
        • 5.3.1 epev 重用
      • 6、epoll的工作模式
        • 6.1 LT 模式
        • 6.1.1 LT 模式介绍
        • 6.1.2 代码案例
        • 6.2 ET 模式
        • 6.2.1 ET 模式介绍
        • 6.2.2 代码案例
        • 6.2.3 代码改进
    • UDP 通信
  • Linux
  • Linux 网络编程
JackCin
2023-09-12
目录

IO多路复用

# IO多路复用

# 1、BIO 和 NIO 模型

(31条消息) 从IO模型到协程(二) BIO模型和NIO模型_张柏沛的博客-CSDN博客 (opens new window)

# 1.1 BIO 模型

阻塞等待

BIO模型

# 1.2 NIO 模型

非阻塞,忙轮询

NIO模型

# 2、I/O多路转接(复用)

  • I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的 系统调用主要有 select、poll 和 epoll。
  1. select / poll

poll)

  1. epoll

IO多路转接(epoll)

# 3、select()

  • 主旨思想:

    1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。

    2. 调用一个系统函数(select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回。

      a. 这个函数是阻塞

      b. 函数对文件描述符的检测的操作是由内核完成的

    3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。

// sizeof(fd_set) = 128 128字节,一个字节8为,总共就是1024位
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
	- 参数:
		- nfds : 委托内核检测的最大文件描述符的值 + 1
		- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
			- 一般检测读操作
			- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
			- 是一个传入传出参数
		- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
			- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
		- exceptfds : 检测发生异常的文件描述符的集合
		- timeout : 设置的超时时间
            
			struct timeval {
				long tv_sec; /* seconds */
				long tv_usec; /* microseconds */
			};

		- NULL : 永久阻塞,直到检测到了文件描述符有变化
		- tv_sec = 0 tv_usec = 0, 不阻塞
		- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
	- 返回值 :
		- -1 : 失败
		- >0(n) : 检测的集合中有n个文件描述符发生了变化
            
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
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

# 3.1 select 工作过程分析

select()工作过程分析

  • ① 先将我们要检测的文件描述符对应的标志位置 1
  • ② 调用 select ,select会将文件描述符列表拷贝到内核态(每次陷入要保存现场,一直在内核态就可以不用频繁地保存、恢复现场),根据我们设定的timeout 检测集合中有n个文件描述符发生了变化,
    • 上图举例:如果读缓冲区内检测到有数据,那么给标志位仍为1,如果没数据,则置为0
    • 补充:select 第一个参数为什么要加1?
      • 是因为底层是 for(i=0,i<101+1,i++),所以要想检测到文件描述符101就必须 +1 才行
  • ③ 检测完会将修改后的列表从内核态拷贝到用户态(即会改变原来我们设定的列表)
  • ④ 以上我们select 就结束了,我们就知道了有几个文件描述符发生了改变,然后就要我们自己for循环去找对应标志位为1的文件描述符,并进行操作
  • 注意:列表的前3位对应的是3个标志IO

# 3.2 代码示例

// select.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main()
{

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    // 因为调用select后会改变原先我们设置的列表,所以我们再创建一个tmp 列表去给select函数调用
    fd_set rdset, tmp;
    // 先清空
    FD_ZERO(&rdset);
    // 将lfd 对应的标志位置位 1
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while (1)
    {
        // 每次循环,都重新设置tmp
        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if (ret == -1)
        {
            perror("select");
            exit(-1);
        }
        // 其实是不会等于 0 的,因为我们select()里最后的参数设置为了 NULL,即 永久阻塞,直到检测到了文件描述符有变化
        else if (ret == 0)
        {
            continue;
        }
        else if (ret > 0)
        {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if (FD_ISSET(lfd, &tmp))
            {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }
            // 打开一个服务器,监听的文件描述符肯定是在最前面,所以要从监听的文件描述符后面开始遍历
            for (int i = lfd + 1; i <= maxfd; i++)
            {
                if (FD_ISSET(i, &tmp))
                {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if (len == -1)
                    {
                        perror("read");
                        exit(-1);
                    }
                    // 说明客户端断开了连接
                    else if (len == 0)
                    {
                        printf("client closed...\n");
                        // 关闭该文件描述符
                        close(i);
                        // fd_set中不再监测这个文件描述符
                        FD_CLR(i, &rdset);
                    }
                    else if (len > 0)
                    {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}
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
// client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if (ret == -1)
    {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1)
    {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if (len == -1)
        {
            perror("read");
            return -1;
        }
        else if (len > 0)
        {
            printf("read buf = %s\n", sendBuf);
        }
        else
        {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
        // usleep(1000);
    }

    close(fd);
    return 0;
}
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

# 3.3 select 的缺点及与poll对比

select-的缺点

  • poll 可以自定义一个结构体数组

# 4、poll()

# 4.1、poll函数

#include <poll.h>
struct pollfd {
	int fd; /* 委托内核检测的文件描述符 */
	short events; /* 委托内核检测文件描述符的什么事件 */
	short revents; /* 文件描述符实际发生的事件 */
};
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
	- 参数:
		- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
		- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
		- timeout : 阻塞时长
			-0 : 不阻塞
			-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
			>0 : 阻塞的时长
	- 返回值:
		-1 : 失败
		>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

poll事件

// poll.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>

int main()
{

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for (int i = 0; i < 1024; i++)
    {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;

    while (1)
    {

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if (ret == -1)
        {
            perror("poll");
            exit(-1);
        }
        else if (ret == 0)
        {
            continue;
        }
        else if (ret > 0)
        {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if (fds[0].revents & POLLIN)
            {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for (int i = 1; i < 1024; i++)
                {
                    if (fds[i].fd == -1)
                    {
                        fds[i].fd = cfd;
                        // 有可能之前的客户端已经断开连接了 但events修改过,所以要重新赋值
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            // 注意poll是按数组下标来遍历的
            // 因为除开我们第一的第一个数组(即监听用的文件描述符),所以从1开始
            // select 是直接按文件描述符来遍历的,所以是从lfd + 1 开始遍历
            for (int i = 1; i <= nfds; i++)
            {
                // 因为 revents 中可能不止一个事件,所以要用 & 判断是否有 POLLIN 事件
                if (fds[i].revents & POLLIN)
                {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if (len == -1)
                    {
                        perror("read");
                        exit(-1);
                    }
                    else if (len == 0)
                    {
                        printf("client closed...\n");
                        // 先close 关闭,再将 fds[i].fd 置为 -1
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    }
                    else if (len > 0)
                    {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

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
//client.c

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        usleep(1000);
    }

    close(fd);

    return 0;
}

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

# 4.2、改进

# 4.2.1 改进一

  • 更新最大文件描述符的索引语句是否应该修改?
    • 老师的代码里,更新语句写的是 nfds = nfds > cfd ? nfds : cfd,这样写之所以可以的原因是 最大的文件描述符总是大于等于它所存储的下标。
    • 例如:因为文件描述符0~2是3个标准,然后那lfd(监听)就为3,这样的话,我们连接了一个客户端,那给它分配的文件描述符就应该是 4,但是我们放到我们自定义的数组里就是第0位,所以用老师的写法虽然可以,但是并不准确
    • 这里应该改成 nfds = nfds>i ? nfds:i 和下标进行对比

# 4.2.2 改进二

  • 当数组满了的时候,又有客户端连接进来,但是accept 已经建立连接分配了文件描述符,但是数组已满,accept返回的文件描述符无法加入到 fds 中,那么下一次循环时该文件描述符的值(代码中的 cfd )已经被丢弃了。
    • 解决:这里应该先去判断数组是否可用,有的话就accpet连接,没有就下一次在处理。

# 4.3、思考

# 4.3.1 思考一

  • 文件描述符初始化为 -1是说明这个文件描述符处于空闲状态吗?
    • 初始化 -1是为了后续好根据这个值判断是否可用。

# 4.3.2 思考二

  • 为什么是用 & 来检测事件?

    //假设:
    POLLIN 的二进制表示为			0000 0000 0000 0001
    POLLOUT 的二进制表示为			0000 0000 0000 0010
    revents 对应事件的二进制	    1111 1111 1111 1111
    //那要判断是否执行了 POLLIN 事件 ,就要 
        revents & POLLIN ==> 0... 0001
    // 如果直接用  == 来判断就会出错
    
    1
    2
    3
    4
    5
    6
    7

# 4.3.2 思考三

  • if(fds[0].revents & POLLIN)和 if(fds[0].revents == POLLIN)不都是要判断是否为 POLLIN 事件吗?作用不是一样吗?
    • 检测不仅仅检测POLLIN,万一也检测POLLOUT呢,如果两个事件都发生了,那 revents 中就包含 POLLIN 和 POLLOUT ,也就是值是 POLLIN | POLLOUT,用 == 去和 POLLIN 判断肯定不对。

# 5、epoll()

epoll图解

# 5.1 epoll 相关函数

#include <sys/epoll.h>
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
	- 参数:
		size : 目前没有意义了。随便写一个数,必须大于0
	- 返回值:
		-1 : 失败
		>0 : 文件描述符,操作epoll实例的

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

struct epoll_event {
	uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:
	- EPOLLIN  //有数据写入 (检测到有数据写入我们才可以读取,所以可以理解为检测读事件)
	- EPOLLOUT //有数据要写  
	- EPOLLERR
    
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- op : 要进行什么操作
			EPOLL_CTL_ADD: 添加
			EPOLL_CTL_MOD: 修改
			EPOLL_CTL_DEL: 删除
		- fd : 要检测的文件描述符
		- event : 检测文件描述符什么事情
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- events : 传出参数,保存了发送了变化的文件描述符的信息
		- maxevents : 第二个参数结构体数组的大小
		- timeout : 阻塞时间
			- 0 : 不阻塞
			- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
			- >0 : 阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数 > 0
		- 失败 -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
47
48
49

# 5.2 epoll基础案例

// epoll.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main()
{

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    // 返回值是一个操作 epoll 示例的文件描述符
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN; // 监听写入事件
    epev.data.fd = lfd;
    // 将示例加入到创建的 epoll 结构中,进行管理
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    // 设定就绪列表的大小
    struct epoll_event epevs[1024];

    while (1)
    {
        // 检测函数,返回发送变化的文件描述符的个数,-1 为不阻塞
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if (ret == -1)
        {
            perror("epoll_wait");
            exit(-1);
        }
        // 输出检测到的发生变化的个数
        printf("ret = %d\n", ret);

        for (int i = 0; i < ret; i++)
        {

            int curfd = epevs[i].data.fd;
            // 监听文件描述符有被写入,说明有客户端连接进来
            if (curfd == lfd)
            {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // epev 是可以重用的,所以直接使用修改后重新添加到 epoll里就行
                epev.events = EPOLLIN;
                //下面写法可以增加要检测的事件,但是这样写记得对不同事件都要进行处理
                // epev.events = EPOLLIN | EPOLLOUT;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else
            {
                // 注意: 如果我们在设置监听事件时,监听了几个事件,那我们在处理时,必须对不同事件都就行处理
                if (epevs[i].events & EPOLLOUT)
                {
                    continue;
                }
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if (len == -1)
                {
                    perror("read");
                    exit(-1);
                }
                // 客户端断开
                else if (len == 0)
                {
                    printf("client closed...\n");
                    // 将用来与断开的客户端通信的文件描述符从epoll里删除,不再监听
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    // 再关闭文件描述符
                    close(curfd);
                }
                else if (len > 0)
                {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }
    // 关闭监听文件描述符
    close(lfd);
    // 使用epoll_create 创建出来的 epfd 也是文件描述符,需要关闭
    close(epfd);
    return 0;
}

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
// client.c

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if (ret == -1)
    {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1)
    {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if (len == -1)
        {
            perror("read");
            return -1;
        }
        else if (len > 0)
        {
            printf("read buf = %s\n", sendBuf);
        }
        else
        {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        // 这里 1000微秒是为了看出 epoll 检测到发生变化个数的效果
        // 如果这里用 sleep 休眠时间太长,看起来就是一次只检测到一个
        // 可能是电脑配置问题,老师视频用 1000可以看出效果,但是我用1000,直接就不输出检测到的发生变化的个数了,改成10000又太大,输出发现只看得到一个,所以我这里用了 5000
        usleep(5000);
    }

    close(fd);
    return 0;
}
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

epoll代码

  • 这里是打开了两个客户端,可以看到有时只有一个被写入,有时有两个同时被改变

# 5.3 补充

# 5.3.1 epev 重用

  • 上面epoll.c 文件中,struct epoll_event epev 定义的 epev 之所以能够重用的原因?
    • 当调用epoll_ctl函数将文件描述符添加到epoll对象中时,epoll会将epoll_event结构体中的数据拷贝一份,存储在自己的内存空间中,并将这个拷贝的结构体作为一个节点插入到红黑树中。
    • 这样做的好处是,当文件描述符上的事件发生时,epoll可以直接从自己的内存空间中获取相应的事件信息,而不需要每次都去访问用户空间中的epoll_event结构体。这样可以提高效率,减少系统调用的次数。
    • 值得注意的是,在将文件描述符从epoll对象中删除时,epoll并不会自动释放之前拷贝的epoll_event结构体,需要用户自己负责释放。

# 6、epoll的工作模式

# 6.1 LT 模式

# 6.1.1 LT 模式介绍

  • LT 模式 (水平触发)
    • 假设委托内核检测读事件 -> 检测fd的读缓冲区
    • 读缓冲区有数据 - > epoll 检测到了会给用户通知
      • a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
      • b.用户只读了一部分数据,epoll会通知
      • c.缓冲区的数据读完了,不通知
  • 简单理解就是,只要缓冲区内有数据,就会发通知

LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这 种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操 作。如果你不作任何操作,内核还是会继续通知你的。

# 6.1.2 代码案例

// epoll_lt.c
// 和epoll基础案例中 epoll.c 的代码对比就只是将通信时的缓冲区buf大小改为5,以方便看效果

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main()
{

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while (1)
    {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if (ret == -1)
        {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for (int i = 0; i < ret; i++)
        {

            int curfd = epevs[i].data.fd;

            if (curfd == lfd)
            {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else
            {
                if (epevs[i].events & EPOLLOUT)
                {
                    continue;
                }
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if (len == -1)
                {
                    perror("read");
                    exit(-1);
                }
                else if (len == 0)
                {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else if (len > 0)
                {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }

    close(lfd);
    close(epfd);
    return 0;
}
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
// client.c
// 将sprintf 固定的输入改为用 fgets 阻塞,键盘输入数据

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if (ret == -1)
    {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1)
    {
        char sendBuf[1024] = {0};
        // sprintf(sendBuf, "send data %d", num++);
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if (len == -1)
        {
            perror("read");
            return -1;
        }
        else if (len > 0)
        {
            printf("read buf = %s\n", sendBuf);
        }
        else
        {
            printf("服务器已经断开连接...\n");
            break;
        }
    }
    close(fd);
    return 0;
}

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

epoll的-LT模式

  • 第一个 ret = 1 是打开了一个客户端,监听的文件描述符的读缓冲区收到了客户端发来的信息(3次握手),所以一打开一个客户的就输出了 ret = 1

  • 注意:如果我们终止客户端,那服务器端还会再打印 ret = 1,(应该是4次挥手,发送了要结束的信息,个人理解处理结束信息的是通信用的文件描述符,不然不服务器端不会执行 len = 0的里的操作)

  • 可以看出,我们只要读缓冲区中还有数据,epoll就会发送通知

  • (佛了,有时会出现 Connection reset by peer的错误 )

# 6.2 ET 模式

# 6.2.1 ET 模式介绍

  • 上面 epoll_lt.c 代码在设置 epev.events 事件时,设置 EPOLLET 即可设定为ET模式
// 下面设置只在有客户端连接,accpet分配的文件描述符时设置,不需要对 lfd 监听描述符设置
epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
1
2

epoll-的ET模式

  • 可以看到 ET 模式 只会通知一次,所以要想将数据都输出出来就必须在接收到一次通知时,循环读取,知道读缓冲为空,但是阻塞情况下,读缓冲区为空,read会阻塞,所以为了解决这个问题,我们还需要将 read 设置为非阻塞

# 6.2.2 代码案例

// epoll_et.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main()
{

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while (1)
    {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if (ret == -1)
        {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for (int i = 0; i < ret; i++)
        {

            int curfd = epevs[i].data.fd;

            if (curfd == lfd)
            {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                // 每次监听文件描述符被写入,就说明有客户端连接,accept分配文件描述符
                // 所以在这里就将文件描述符设置为非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else
            {
                if (epevs[i].events & EPOLLOUT)
                {
                    continue;
                }

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while ((len = read(curfd, buf, sizeof(buf))) > 0)
                {
                    // 打印数据
                    //这里用printf 或者 write都可以,printf就记得加\n,刷新缓冲区,write就可以让数据一行显示
                    printf("recv data : %s\n", buf);
                    // write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if (len == 0)
                {
                    printf("client closed....\n");
                }
                else if (len == -1)
                {
                    if (errno == EAGAIN)
                    {
                        printf("data over.....\n");
                    }
                    else
                    {
                        perror("read");
                        exit(-1);
                    }
                }
            }
        }
    }

    close(lfd);
    close(epfd);
    return 0;
}
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
// client.c

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if (ret == -1)
    {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1)
    {
        char sendBuf[1024] = {0};
        // sprintf(sendBuf, "send data %d", num++);
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if (len == -1)
        {
            perror("read");
            return -1;
        }
        else if (len > 0)
        {
            printf("read buf = %s\n", sendBuf);
        }
        else
        {
            printf("服务器已经断开连接...\n");
            break;
        }
    }
    close(fd);
    return 0;
}
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

epoll的ET模式

  • 非阻塞的情况下,如果数据已经被读完了,然后再去 read 读数据,就会产生 EAGAIN ,注意:这种情况只会在非阻塞情况下出现,因为阻塞情况下,数据读完了,read 就阻塞了(老师说的)

  • 这个代码bug蛮多的,而且一段时间正常一段时间又出bug,主要的bug还是在客户端主动结束后,服务器端会报错,打印 Connection reset by peer的错误

  • 评论区:

    • 当 read 函数在非阻塞模式下读取数据时,如果缓冲区中没有数据可读,返回值是 -1,并且 errno 变量被设置为 EAGAIN 或 EWOULDBLOCK,表示当前没有数据可读
    • 新版本缓冲区没数据可读返回的是0,并且设置errno为EAGAIN

# 6.2.3 代码改进

  • epoll 检测到可写的时候有问题, 检测了是否可写事件,然而,可写是一直可写的,当一个文件描述符,可写并可读的时候,却被continue了。

  • epevs[i].events & EPOLLOUT &&  (( epevs[i].events & EPOLLIN ) != 1)
    
    1
  • 大概就这个意思,还没具体去测试这个上面这个对不对

编辑 (opens new window)
上次更新: 2023/09/13, 12:29:52
socket通信基础
UDP 通信

← socket通信基础 UDP 通信→

最近更新
01
51单片机及补充知识
09-13
02
独立按键
09-13
03
LCD1602液晶显示器
09-13
更多文章>
Theme by Vdoing | Copyright © 2019-2023 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式