2.1.2 事件驱动reactor的原理与实现

LINUX 精通 2

day14 20240513
day15 20240514
算法刷题:2维前缀和,一二维差分 耗时 135min
习题课 4h

课程补20240425 耗时:4h

课程链接地址

回顾

  1. 怎么学0voice课
  2. 网络io——一请求一线程,一个client一个连接再accpet分配io fd文件描述符

注意:rm -rf是一个非常强大和危险的命令,它会递归地删除目录及其内容,而不进行任何确认。请谨慎使用此命令,以免意外删除重要文件或系统关键组件

每次上课前统一一下代码

  1. gitlab.0voice.com

    找到代码,然后在Linux终端里 git clone 可以挂梯子, 用户名是邮箱

    在本地下连ftp shell好像不太行

  2. gcc -o networkio networkio.c -lpthread

问题

  1. client断开后问题

    运行以后./networkic 连三个 发三个,然后断开一个,会一直send recv 一瞬间cpu占用100%, 因为没有处理断开的

我没碰到,因为老师改过了void *client_thread加了count==0处理的

if (count == 0) { // disconnect
			printf("client disconnect: %d\n", clientfd);
			close(clientfd);
			break;
		}
  1. fd就是网络io 是int型

    开了sockfd在这里插入图片描述

    fd(不论是sockfd 还是clientfd) 从3开始,0 1 2系统默认stdin stdout stderror;往上增加

    ls /dev/fd
    

    在这里插入图片描述

    看ulimit -a文件描述符fd数量max (open files)

    在这里插入图片描述

    为什么能一直增加:

    linux下操作,一切皆是文件FD(file descriptor)

    可以隔段时间再send

  2. client断开后,隔段时间再连接,fd变了吗

    变了

    disconnect以后就 close(clientfd)了!!!!

    4没了,被回收了,变成了7

    等一段时间,又变成4了

    io回收时间 系统默认60s,set可以设置time_wait

2.1.2 事件驱动reactor的原理与实现

还是用我自己的版本

一请求一线程

优点:代码逻辑简单

缺点:不利于并发 1k,通过创建线程实现并发

所以用多路复用io

调试技巧

在命令行打man select函数名 就能看解释

select poll epoll

fdset
  1. 到底是什么东西?

    1. 答案:它是比特位集合

      把fd放一起的set集合

      干嘛的:比如你时间管理大师,处了好多个fd 对象

    2. 为什么要设置一个集合fdset, 然后传进select,传来传去

      传入3456,系统返回34可读

    在这里插入图片描述

    ​ 所有通讯底层的server io多路复用都是这么写的

    ​ 云里雾里的头痛,乱七八糟的,send可以,但是收不到!!!!我 爆炸了裂开

    现在是main里一个线程,多路io fd,fd间不影响!!

    ​ 可读返回,不可读阻塞在select,对着标准答案改了终于行了

    1. fd_set结构体

      select头文件里就1个struct, 4个宏定义,1个函数不难的

      宏定义FD_ZERO, FD_SET, FD_CLR,

      select()函数的参数:可读 可写,错误,

      timeout=null 默认一直阻塞, 如果阻塞超过市场就往下走,可以做一个定时器,就是为了切换线程,等待就绪再次被执行

  2. 大小

    fd_setsize大小可以改

    在posix_types.h里看到

    在这里插入图片描述

    fd_setsize = 1024

    8因为一个字节byte = 8bit

    sizeof(long)因为前面是unsigned long 所以要除它, 假设long是4 byte

    所以一个fd大小是1024/(8*4) = 32 byte

select

特点/运行机制

  1. 每次调用select需要把fd_set集合,从用户空间copy到kernel内核空间

  2. maxfd, 为了遍历fd是否set置一了,设置的最大的fd,需要人为设置

    for( int i = 0; i< maxfd; i ++)

    ps:rfds, rset区别

fd_set rfds, rset; 
//rfds返回数据dataset, 是应用层的,用户设置的
// rset是复制rfds的, 用于被复制到内核空间,用于判断的

优点:实现只用一个thread进程就能多路io复用

缺点: 参数太多,麻烦

 
#else

    fd_set rfds, rset; 
    //rfds返回数据dataset, 是应用层的,用户设置的
    // rset是复制rfds的, 用于被复制到内核空间,用于判断的

    FD_ZERO(&rfds); //先清空
    FD_SET(sockfd, &rfds); // 再把sockfd 设置在可读rfds里,置1

    int maxfd = sockfd; //用来方便遍历set用到的最大值

    while(1){
        rset = rfds; //关联,


        // maxfd +1因为下标从0开始,数量比最后多1
        //  int select(int nfds, fd_set *readfds, fd_set *writefds,
        //   fd_set *exceptfds, struct timeval *timeout);
        int nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //返回就绪的fd数量,就绪的bit位是1

        if(FD_ISSET(sockfd, &rset)){//sockfd位是否置1
            // accept 如果监听的sockfd置1了,就开始accept连接
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("accept finished: %d\n", clientfd);

            FD_SET(clientfd, &rfds); //有一个fd,就set一下,maxfd也变了
            if(maxfd < clientfd) maxfd = clientfd; //clientfd当client断连了,会被回收,所以要判断一下
        }

        // recv
        int i = 0;
        for(i = sockfd +1; i<= maxfd; i++){ //i就是fd
            if (FD_ISSET(i, &rset)){//判i可读rset吗
                char buffer[1024] = {0};
                int count = recv(i, buffer, 1024, 0); //Read N bytes into BUF from socket FD.
                
                if (count == 0) { // disconnect
                    printf("client disconnect: %d\n", i);
                    close(i);
                    FD_CLR(i, &rfds); //FD_CLR是一个宏,用于从fd_set数据结构中清除指定的文件描述符
                    continue;
                }
		        
				printf("RECV: %s\n", buffer);

				count = send(i, buffer, count, 0);
				printf("SEND: %d\n", count);
			}
		}
	}

#endif
poll
  1. struct pollfd里

    比如上面传入3456 只返回34可读

    fd 是哪一个fd

    events 传入的事件

    revents 返回的事件

    基本代码和select差不多,就是4个宏还有select函数要改成poll才有的 写法

    别进错文件夹编译

    gcc -o networkio networkio.c
    ./networkio
    

    开三个网络助手——connect——send——recv吗

    在这里插入图片描述

    成功啦啦啦啦啦啦啦

  2. 总结poll有什么呢

    1. pollfd是个结构体:fd 是哪一个fd;events 传入的事件;revents 返回的事件

    2. 宏定义:pollin可读,pollout等等

    3. poll函数是系统调用 每次把fds copy到内核kernel里

      系统用for遍历maxfd个数量,判断这个io fd是否就绪

  3. poll有什么独特使用场景

    1. 底层逻辑类似select参数更少

    2. 问: 假设5个fd一起来,阻塞,假设都不能可读一直等,直到1可读立即返回?

      答:不对,因为内核做不了微秒级,第一次与第二次进while的select往下几乎没有处理上的差别

      没懂???

#else
    //  进pollfd 看参数
    struct pollfd fds[1024]= {0}; //fdset就是
    fds[sockfd].fd = sockfd;
    fds[sockfd].events= POLLIN; //pollin就是可读,设置为POLLIN表示对该文件描述符上是否有可读数据感兴趣

    int maxfd = sockfd; //来遍历用的,检查哪个fd set了 
    while(1){
        int nread = poll(fds, maxfd + 1, -1);//set, set大小, timeout =-1一直阻塞等待
        if (fds[sockfd].revents & POLLIN){
            // pollin是x十六进制0x0001,变成8位2进制00000001
           
           
            // 如果有可读的,用accept处理分配io,复制上面
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("accept finished: %d\n", clientfd);

            // FD_SET(clientfd, &rfds); //select的这句改成poll的下面两行
            fds[clientfd].fd= clientfd;
            fds[clientfd].events=POLLIN;

            if(maxfd < clientfd) maxfd = clientfd; //clientfd当client断连了,会被回收,所以要判断一下
        }

            // 抄上面recv
            int i = 0;
            for(i = sockfd +1; i<= maxfd; i++){ //i就是fd
                // if (FD_ISSET(i, &rset)){//判i可读rset吗 select有,poll没有
                if(fds[i].revents & POLLIN){ //判i可读吗,和Pollin位与
                    char buffer[1024] = {0};
                    int count = recv(i, buffer, 1024, 0); //Read N bytes into BUF from socket FD.
                    
                    if (count == 0) { // disconnect
                        printf("client disconnect: %d\n", i);
                        close(i);
                        // FD_CLR(i, &rfds); //FD_CLR是fdset里的一个宏,select有;poll没有
                        fds[i].fd= -1; //因为从0开始,置-1
                        fds[i].events= 0;
                        continue;
                    }
                    
                    printf("RECV: %s\n", buffer);

                    count = send(i, buffer, count, 0);
                    printf("SEND: %d\n", count);
                }
            }
    }
epoll

linux 2.4以前,没有听过linux做server的,也没有云主机。当时server都是Windows,unix,十几年后现在云主机很多系统都是Linux,因为linux2.6以后引入epoll,server对io的数量更多

为什么?select与poll底层都需要进while的select/poll阻塞检查,再for判断 accept recv ,epoll不用

#else
    int epfd = epoll_create(1);

    struct epoll_event ev; //构建事件,只用来add和delete,control里没用
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); //control


    while(1){
        struct epoll_event events[1024] = {0};
        int nready = epoll_wait(epfd, events, 1024, -1);

		int i = 0;
		for (i = 0;i < nready;i ++) {
			int connfd = events[i].data.fd;
			if (connfd == sockfd) {
                
                
                // accept				
				int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
				printf("accept finshed: %d\n", clientfd);
                // 创建events, 添到ctl里
				ev.events = EPOLLIN;
				ev.data.fd = clientfd;
                // 这里ev不写也可以
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
				
			} else if (events[i].events & EPOLLIN) {

				char buffer[1024] = {0};
				
				int count = recv(connfd, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("client disconnect: %d\n", connfd);
					close(connfd);
                    // 改了这里
					epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
					// epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev); 也可以
					
					continue;
				}

				printf("RECV: %s\n", buffer);

				count = send(connfd, buffer, count, 0);
				printf("SEND: %d\n", count);

			}

		}

	}
	
	

总结

  1. 一个struct结构体+三个函数给应用层提供的接口

    在这里插入图片描述

  2. int epoll_create(int size);知识点1:epoll_create返回传入都是int。

    epoll一开始size代表一次性就绪的io数量,后来就绪从数组改成链表,此时size没作用只要size不为0,效果都一样。 size为了兼容老版本留下来了

  3. 为什么epoll不用遍历?

    住户是IO或者fd,epoll类似快递员,但是去丰巢,由io自己去丰巢,事件events 有两个= 收+寄;poll、select是每家敲门,没法时时敲门还

    在这里插入图片描述

    1. epoll_create:用struct组织200总集200个住户IO;用struct组织快递柜丰巢——就绪,并聘请快递员
    2. epoll_ctl: 住户搬走EPOLL_CTL_DELET,搬进ADD,换楼层位置MOD
    3. epoll_wait: 快递员多久去一次丰巢,timeout市场;events是小车取出就绪内容,maxevent是小车多大
  4. 为什么能大并发?

    1. select(maxfd, rfds, wfds, efds, err);五个参数

      if 100万 io,需要把100万全部copy到对应fds里判断可读、写…

    2. 但是epoll不用每次从用户应用层copy到内核里,epoll create是一个个添加到内核里积累起来,有读写事件来了,wait就从就绪里操作

    3. 就绪里是真正处理的事件:微信号称3亿用户同时在线,就是IO整集大小,但是不代表server处理同时发消息

      每个client对应io,可能就绪队列在发消息的才一百万不到

  5. 思考题:

    1. 整集用什么数据结构存
      • 数组:将整数按顺序存储在数组中,并通过索引访问。这种方式简单直接,但插入和删除操作的时间复杂度较高。
      • 链表:将每个整数存储在链表节点中,并使用指针连接节点。这种方式对于频繁的插入和删除操作更为高效,但随机访问的性能较差。
      • 哈希集合:利用哈希函数将整数映射到不同的桶中,在每个桶内使用链表或红黑树来处理冲突。这种方式可以实现快速查找、插入和删除操作。
    2. 就绪用什么数据结构存
      • 位图(Bitmask):使用一个二进制位代表一个文件描述符,当某个文件描述符就绪时,相应位设置为1。该方法适用于文件描述符数量较少且连续排列的情况。
      • 数组:将就绪的文件描述符存储在数组中。该方法适用于文件描述符数量不多且无需频繁变动的情况。
      • 数据结构依赖具体的多路复用机制:例如,epoll使用红黑树来管理就绪事件,将文件描述符作为节点进行存储;而select和poll则使用fd_set结构体数组来存储就绪文件描述符。

引出reactor反应堆

  1. epoll:io数量很多;

    poll:io少, <10

    select: 无poll epoll 比如Linux2.4前

  2. 都是对网络应用层的IO事件处理4种,根本没有对用户层的业务service处理还,只处理了IO里的事件,告诉你吃饭events事件了,没告诉你什么饭怎么吃具体的service

  3. 过程:server listen——client connect——server accept——client send ——server receive同时回传

  4. 事件:

    所以是IO事件触发——水平触发,边沿触发

    核心是events事件,一个IO的生命周期=无数多个events

    server关心events,不是具体io,对不同events执行不同callback回调函数cb,模块化!!!

  5. reactor:核反应堆=不同IO事件处理callback回调函数 cb的集合

    封装起来,给以后用户层service用,更符合人的逻辑

    **什么是reactor:**注册一个events,当events发生,就从reactor查返回回调函数,做反应(类似留下名片只要有事就call 你)

    为什么要用reactor因为可以更好关注事件而不是io,io太多也不是都活着,从io管理➡️ 事件管理

网络IO

  1. accept——》listenfd/sockfd
  2. send/recv——》clientfd

网络应用是所有服务的基石

ps:

要用形象的东西,记忆更牢,也不反人性,少掉头发少烧脑

5/15晚上把github同步自己代码搞定!!!

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值