【文末送书】计算机网络 | IO多路转接技术 | poll/epoll详解

在这里插入图片描述

欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起学习和分享Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。


IO多路转接技术 —— poll/epoll详解

    • 1. poll详解
    • 2. epoll详解
      • (1)API介绍
      • (2)epoll树
      • (3)epoll模型
      • (4)epoll的三种工作模式
      • (5)文件描述符1024限制
    • 图书推荐 -《构建高性能嵌入式系统》


🎉🎉🎉🎉🎉 重磅福利 🎉🎉🎉🎉🎉🎉
🎉本次送1本书 ,评论区抽1位小伙伴送书🎉
🎉活动时间:截止到 2023-11-30 10:00:00🎉
🎉抽奖方式:评论区随机抽奖。
🎉参与方式:关注博主、点赞、收藏,评论。
❗注意:一定要关注博主,不然中奖后将无效!
🎉通知方式:通过私信联系中奖粉丝。
💡提示:有任何疑问请私信公粽号 《机器和智能》
在这里插入图片描述


专栏:《Linux从小白到大神》《网络编程》


1. poll详解

  • 函数原型

    int poll(struct pollfd *fd, nfds_t nfds, int timeout);
    
  • 函数参数

    • fd:数组的地址,struct pollfd all[120]; 其中struct pollfd结构体如下

      struct pollfd {
      			int   fd;         /* 文件描述符 */
      	     	short events;     /* 等待的事件 */
      	     	short revents;    /* 实际发生的事件 */
      };
      

      结构体红各项含义如下:

      • 文件描述符fd:表示要坚持测的fd,通过 open(“a.txt”, O_wronly | O_append); 获得。

      • events:要等待的事件

        在这里插入图片描述

      • revents:实际发生的事件,它是内核给的反馈,在select的时候,会有一个备份来供内核修改并传出。

    • nfds:数组的最大长度, 数组中最后一个使用的元素下标+1

      • 内核会轮询检测fd数组的每个文件描述符
    • timeout:

      • -1:永久阻塞
      • 0:调用完成立即返回
      • >0:等待的时长毫秒
  • 函数返回值:IO发生变化的文件描述符的个数。

2. epoll详解

(1)API介绍


int epoll_create(int size);
  • 函数功能:生成一个epoll专用的文件描述符,实际上就是生成一个epoll树的根结点。
  • 函数参数:size,epoll树上能挂的最大文件描述符数量。表示我想在这个树节点上挂size个节点,假如实际上的节点大于size的话epoll会自动扩展,所以这个大小可以随便传,不用太在意。但是这个扩展也是有上限的,如果电脑内存是1G,那么扩展的上限是10万(2G就是20万。。。通过加内存可以扩大上限)。
  • 函数返回值:函数返回值是树的根节点,在后面用到epft参数的时候,都是指这个返回值,也就是树的根节点。

在这里插入图片描述


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 函数功能:用于控制某个epoll文件描述符事件,可以注册、修改、删除。

  • 函数参数:

    • epfd:epoll_create()函数生成的专用文件描述符。

    • op:

      • EPOLL_CTL_ADD —— 注册
      • EPOLL_CTL_MOD —— 修改
      • EPOLL_CTL_DEL —— 删除
    • fd:关联的文件描述符

    • event:告诉内核要监听什么事件

      • EPOLLIN —— 读

      • EPOLLOUT —— 写

      • EPOLLERR —— 异常

      • struct epoll_event { /* 该结构体主要存放和fd有关的信息 */
                uint32_t     events;                          
                epoll_data_t data; 
        };
        
        typedef union epoll_data {
              	void         *ptr;
               	int          fd;
               	uint32_t     u32;
               	uint64_t     u64;
        } epoll_data_t;
        
        • epoll_data_t是一个联合体union,四个成员共用同一块内存,也就是说四个成员我们只能用一个,一般情况下我们用fd,这个fd实际上就是epoll_ctl()函数的第三个参数fd。

        • 如果我们想在epoll树上挂载更多信息,而不仅仅是fd文件描述符的话,我们可以把更多信息封装在结构体中,并把该结构体传给epoll_data_t结构体的ptr指针,这样就可以在epoll树上挂载和fd有关的更多信息。

          struct sockInfo
          {
          	int 				fd;
          	struct sockaddr_in 	addr;
          };
          

          比如说,要获取发生变化的fd对应的client的IP和port,就可以利用指针ptr,这样的话联合epoll_data_t中的fd就不能用了,我们把文件描述符传给sockInfo的fd即可完成fd信息的挂载。


int epoll_wait(
			int epfd,
			struct epoll_event* events,  /* 结构体数组 */
			int maxevents,
			int timeout
            );
  • 函数功能:等待IO事件发生(可以设置阻塞),epoll_wait()函数相当于前面讲的select()或poll()函数,表示委托内核去进行检测。epoll_event通过返回值和传出参数events来实现把哪几个fd发生变化告诉server进程的目的。首先,每当有fd变化,就把这个fd对应的树节点拷贝到events数组中,最后,有几个fd变化,就返回几。这样只要根据返回值和参数events就可以遍历出所有变化的fd以及相关信息。

  • 函数参数:

    • epfd:要检测的句柄

    • events:用于回传待处理事件的数组。它是一个传出参数,需要提前分配内存,哪个fd发生变化了,就把哪个fd的树节点(struct epoll_event)拷贝一份放到这个数组中。这样epoll就能返回是哪个fd发生了变化。

    • maxevents:告诉内核events的大小,因为内核要把发生变化的fd对应的树节点拷贝到数组中,所以要知道数组大小。

    • timeout:为超时时间

      • -1:永久阻塞
      • 0:立即返回
      • >0
  • 函数返回值:有多少个fd发生了变化就返回几(变化的fd信息存在events数组中)。

(2)epoll树

在这里插入图片描述

(3)epoll模型

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

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]); //字符串转整形值

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET; // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
    serv_addr.sin_port = htons(port); // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
        //存放发生变化的fd对应的树节点
    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);

        // 遍历all数组中的前ret个元素 //ret表示有几个变化的fd,变化的fd都存在all数组中
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求 // accept不阻塞,因为已经有连接
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                temp.events = EPOLLIN; //检测cfd对应的读缓冲区,是否有数据传入
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
                
                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %d\n",
                inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    		ntohs(client_addr.sin_port));
                
            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) //只处理读事件
                {
                    continue;
                }
                /*
                假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,
                但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,
                那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,
                epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。
                这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。
				*/

                // 读数据
                char buf[1024] = {0};
                int len = recv(fd, buf, sizeof(buf), 0);
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("client disconnected ....\n");
                    //close(fd);
                    // fd从epoll树上删除
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    // 挂树的时候需要ev,把ev挂在树上删除写NULL就行了
        			if(ret == -1)
        			{
                		perror("epoll_ctl del error");
               	 		exit(1);
        			}
       				close(fd);
                }
                else
                {
                    printf(" recv buf: %s\n", buf);
                    write(fd, buf, len);
                }
            }
        }
    }

    close(lfd);
    return 0;
}

epoll维护的红黑树是存在一个共享内存中,内核和用户都可以通过操作这个共享内存来操作树,不需要内核态和用户态的切换,也不需要两种状态之间的数据拷贝,所以效率更高。

(4)epoll的三种工作模式

  • 水平触发模式 - (根据读来解释)

    • 只要fd对应的缓冲区有数据,epoll_wait就会返回
    • 返回的次数与发送数据的次数没有关系
    • epoll默认的工作模式
  • 边沿触发模式 - ET

    • fd - 默认阻塞属性

    • 客户端给server发数据:

      • 发一次数据server 的 epoll_wait就返回一次

      • 不在乎数据是否读完

      • 如果读不完,如何把数据全部读出来?

        • while(recv());

          • 数据读完之后recv会阻塞
          • 解决阻塞问题 —— 设置非阻塞fd

在这里插入图片描述

对于epoll_wait()来说,epoll_wait 调用次数越多, 系统的开销越大。

水平触发模式会多次返回,只要server的read缓冲区有数据,epoll_wait就返回,也就会通知server去读数据,那么在循环检测的时候,只要server的read缓冲区有数据,epoll_wait就会多次调用,多次返回,并通知server去读数据;假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。 —— (printf函数是标准C库函数,C库函数都有一个默认缓冲区,printf的大小是8K。printf函数是行缓冲,使用printf函数的时候,如果不加 \n 会默认等到写满的时候才打印内容,加 \n 会强制把缓冲区的内容打印出来。另外 \0 表示结束,不加 \0 就会一直输出直到遇到 \0,用write(STDOUT_FILENO)替代printf函数就可以解决这些问题。)

边沿触发模式,client发一次数据epoll_wait只返回一次,也就只读一次,这样的话server的read缓冲区可能会有很多数据堆积,server读数据的时候可能读到的是上一次剩余的数据,并且只有client发的时候,epoll_wait才会通知server去读数据,边沿触发模式尽可能减少了epoll_wait的调用次数,缺点是数据有可能读不完导致堆积;

  • 边沿非阻塞触发
    • 效率最高

    • 如何设置非阻塞

      • open()

        • 设置flags
        • 必须 O_WDRW | O_NONBLOCK
        • 终端文件: /dev/tty
      • fcntl

        • int flag = fcntl(fd, F_GETFL);
        • flag |= O_NONBLOCK;
        • fcntl(fd, F_SETFL, flag);
    • 如何将缓冲区的全部数据都读出?

      while(recv() > 0)

      {

      ​ printf();

      }

    • 当缓冲区数据读完之后, 返回值是否为0?

      • 阻塞状态
        • 数据读完之后,recv阻塞
      • 非阻塞状态
        • 强行读了一个没有数据的缓冲区(fd),数据已经被读完了,因为是非阻塞,所以在while循环中recv还要继续读,导致返回-1
        • 判断 errno == EAGAIN

示例

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

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;

    // 设置边沿触发
    ev.events = EPOLLIN; //监听的文件描述符没必要边沿触发,主要是通信的cfd
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
        printf("================== epoll_wait =============\n");

        // 遍历all数组中的前ret个元素
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 设置文件cfd为非阻塞模式
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                // 设置边沿触发
                temp.events = EPOLLIN | EPOLLET;
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
                
                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %d\n",
                inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                ntohs(client_addr.sin_port));
                
            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) 
                {
                    continue;
                }

                // 读数据
                char buf[5] = {0};
                int len;
                // 循环读数据
                while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
                {
                    // 数据打印到终端
        			//不要用printf,因为printf如果找不到 \0 \n 字符会出现乱码,打印不出来等问题
                    write(STDOUT_FILENO, buf, len);
                    // 发送给客户端
                    send(fd, buf, len, 0);
                }
                if(len == 0)
                {
                    printf("客户端断开了连接\n");
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1)
                {
        			//数据已经被读完了,因为是非阻塞,所以在while循环中recv还要继续读,导致返回-1
                    if(errno == EAGAIN)
                    {
                        printf("缓冲区数据已经读完\n");
                    }
                    else
                    {
        				//这才是真正的recv错误
                        printf("recv error----\n");
                        exit(1);
                    }
                }
#if 0
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("client disconnected ....\n");
                    // fd从epoll树上删除
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                    
                }
                else
                {
                    // printf(" recv buf: %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(fd, buf, len);
                }
#endif
            }
        }
    }

    close(lfd);
    return 0;
}

(5)文件描述符1024限制

对于select来说,无法突破文件描述符1024上限,因为select是通过数组实现的。poll和epoll可以突破1024限制,poll是内部链表实现,而epoll是红黑树实现。

查看受计算机硬件限制的文件描述符上限可以通过下面命令

cat /proc/sys/fs/file-max

同样,我们也可以通过修改配置文件来修改这个上限,但是,我们在程序中设置的时候不能超过硬件限制的上限

vim /etc/security/limits.conf
  • soft nofile 8000 —— 也可以通过命令ulimit -n 2000来修改为2000

  • hard nofile 8000 —— 硬件资源限制

修改后重启系统即可起效。

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uPTgbmD-1682218134943)(Typora_picture_reference/1662196294315.png)]

图书推荐 -《构建高性能嵌入式系统》

在这里插入图片描述
购买链接:点击购买-链接1
点击购买-链接2

内容简介:
《构建高性能嵌入式系统》本书详细阐述了与构建高性能嵌入式系统相关的基本解决方案,主要包括构建高性能嵌入式系统、传感器、实时操作、FPGA项目、KiCad设计电路、构建高性能数字电路、固件开发、测试和调试嵌入式系统等内容。此外,本书还提供了相应的示例、代码,以帮助读者进一步理解相关方案的实现过程。

在这里插入图片描述

前 言
用于家庭、汽车和个人的现代数字设备包含越来越复杂的计算能力。这些嵌入式系统以每秒数千兆位的速率生成、接收和处理数字数据流。本书将教你如何使用现场可编程门阵列(Field Programmable Gate Array,FPGA)和高速数字电路设计技术来设计创建你自己的尖端数字设备。
本书读者
本书适用于软件开发人员、硬件工程师、物联网(IoT)开发人员以及其他任何寻求了解开发高性能嵌入式系统过程的人员。潜在受众包括有兴趣了解FPGA开发基础知识以及 C和C 固件开发所有方面的任何人。读者应当对C/C 语言、数字电路和焊接电子元件等有基本了解。
内容介绍
本书分为3篇,共10章,具体介绍如下。
第1篇:高性能嵌入式系统的基础知识,包括第1~3章。
第1章“高性能嵌入式系统”,详细阐释了嵌入式系统架构的元素,并讨论了在各种嵌入式应用中通用的一些关键系统特性。嵌入式系统通常包括至少一个微控制器或微处理器、传感器、执行器、电源,在许多情况下,还会有一个或多个网络接口。本章还深入探讨了嵌入式系统和物联网之间的关系。
第2章“感知世界”,详细介绍了在各种嵌入式应用中使用的传感器的原理和实现。无源传感器可测量周围环境的属性,如温度、压力、湿度、光强度和大气成分等。有源传感器则可以使用雷达和激光雷达等能量发射技术来探测物体并测量其位置和速度。此外,本章还介绍了与传感器通信的接口。
第3章“实时操作”,探讨了嵌入式系统对从传感器和其他来源测量的输入生成实时响应的需求,介绍了实时操作系统(RTOS)的概念及其关键特性,以及在实时应用程序中实现多任务处理时常见的一些挑战。此外,本章还介绍了一些流行的开源和商业RTOS实现的重要特征。
第2篇:设计和构建高性能嵌入式系统,包括第4~7章。
第4章“开发你的个FPGA项目”,首先讨论了实时嵌入式系统中 FPGA 设备的有效使用,然后阐释了标准FPGA中包含的功能元素。本章介绍了一系列FPGA设计语言,包括硬件描述语言(hardware description language,HDL)、原理图方法和流行的软件编程语言(包括C和C )。本章介绍了FPGA开发过程,并提供了一个FPGA开发周期的完整示例。
第5章“使用FPGA实现系统”,深入探讨了使用FPGA设计和实现嵌入式设备的过程。本章首先介绍了FPGA编译软件工具和编译过程,使用工具可将编程语言中的逻辑设计描述转换为可执行的FPGA配置。本章还讨论了适合FPGA实现的算法类型,后还开发了一个基于FPGA的高速数字示波器基础项目。
第6章“使用KiCad设计电路”,介绍了优秀的开源KiCad电子设计和自动化套件。在KiCad中工作可以使用原理图设计电路并开发相应的印刷电路板布局。你将了解如何以非常合理的成本将电路板设计转变为原型产品。
第7章“构建高性能数字电路”,详细阐释了使用表面贴装和通孔电子元件组装高性能数字电路原型所涉及的过程和技术。本章介绍的电路板组装工具包括焊台、放大镜或显微镜以及用于处理微小零件的镊子等。此外,本章还介绍了回流焊接工艺,并描述了一些用于实现小规模回流能力的低成本选项。
第3篇:实现和测试实时固件,包括第8~10章。
第8章“首次给电路板通电”,介绍了如何为电路板通电做准备。本章将引导你完成首次向电路板供电并检查基本电路级功能的过程。发现任何问题时,可以按本章建议的方法调整电路。在测试通过之后,还可以添加FPGA逻辑,并测试示波器电路板的数字接口。
第9章“固件开发过程”,演示了如何在电路板正常运行后充实FPGA算法的其余关键部分,包括与模数转换器(analog to digital converter,ADC)的通信,以及MicroBlaze处理器固件的开发。在开发固件时,重要的是尽可能对代码进行静态分析,这样可以避免许多难以调试的错误。实现版本控制系统以跟踪项目生命周期中代码的演变也很重要。本章讨论了开发一个全面的、至少部分自动化的测试套件对于在进行更改时保持代码质量的重要性。此外,本章还着重介绍了编码风格。
第10章“测试和调试嵌入式系统”,讨论了嵌入式系统的全面测试问题。系统级测试必须针对整个系统预期范围的环境条件和用户输入(包括无效输入),以确保系统在所有条件下都能正常运行。此外,本章还讨论了有效调试技术,总结了高性能嵌入式系统开发的实践。
充分利用本书
本书充分利用了强大的免费商业和开源软件工具套件来开发FPGA算法和设计复杂的印刷电路板。要跟随本书示例项目学习,你需要一个特定的FPGA开发板Digilent Arty A7-100。要构建数字电路以实现你的设计,你还需要一套用于焊接和拆焊表面贴装元件的工具。此外,你可能还需要一些工具来协助处理精细元件,如精密镊子、放大镜或显微镜等。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/167600.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DAY59 503.下一个更大元素II + 42. 接雨水

503.下一个更大元素II 题目要求&#xff1a; 给定一个循环数组&#xff08;最后一个元素的下一个元素是数组的第一个元素&#xff09;&#xff0c;输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序&#xff0c;这个数字之后的第一个比它更大的数&am…

DDD落地:从美团抽奖平台,看DDD在大厂如何落地?

尼恩说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 谈谈你的DDD落地经验&#xff1f; 谈谈你对DDD的理解&#xff…

【深入Scrapy实战】从登录到数据解析构建完整爬虫流程

文章目录 1. 写在前面2. 抓包分析3. Scrapy提交登陆请求4. 列表与详情页面数据解析5. 中间件Middleware配置 【作者主页】&#xff1a;吴秋霖 【作者介绍】&#xff1a;Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作&#xf…

迈巴赫S480升级镀铬中网 480秒变680

迈巴赫S480的首选项目&#xff0c;就是前杠格栅升级了&#xff0c;只需要替换前杠下方的三个格栅中网&#xff0c;就可以直接升级成迈巴赫S680的外形&#xff0c;立马提升了两个档次。

SAP ABAP给指定用户增加SAP ALL权限

下面的例子是给指定用户增加SAP ALL的权限ABAP代码&#xff0c;增加指定权限对像的没研究&#xff0c;只能自己看了。这应该是SAP权限的无限破解了吧。 例子中SAP*,是当前系统中有SAP_ALL权限的一个用户&#xff0c;用来参考使用的&#xff0c;根据实际系统用的最大权限用户&a…

Sqlserver 多行合并为一行

多行合并为一行 表结构 子表结构如下&#xff1a; 父表结构如下&#xff1a; 由图可以看出子表和父表是通过父表ID进行关联的。 我们要实现的效果如下&#xff0c;查询父表数据的同时&#xff0c;增加一列来展示父表下子商品信息。 完整代码如下 select top {0} * from (…

不想花大价钱?这10款替代Axure的平替软件更划算!

Axure是许多产品经理和设计师进入快速原型设计的首选工具&#xff0c;但Axure的使用成本相对较高&#xff0c;学习曲线陡峭&#xff0c;许多设计师正在寻找可以取代Axure的原型设计工具&#xff0c;虽然现在有很多可选的设计工具&#xff0c;但质量不均匀&#xff0c;可以取代A…

壹基金为爱同行走进绿水青山,为乡村儿童送去健康水

壹基金为爱同行公益践行活动发起于2013年,截至2022年底,累计有63,319名线下参与队员,走过了8个城市。2023年,为爱同行的“壹家人”再次出发,走进“绿水青山就是金山银山”理念诞生地——浙江安吉余村,徒步18公里,为乡村儿童喝上足量、干净的饮用水筹集善款。本次活动获得了当地…

【Mysql】基于MySQL协议的抓包工具

mysql_sniffer 是一个基于 MySQL 协议的抓包工具&#xff0c;用来实时抓取 MySQL 服务端的请求&#xff0c;并格式化输出&#xff0c;输出内容包括访问时间、来源 IP、执行的SQL语句。 在进行MySQL 8.0升级时&#xff0c;了解新版本对SQL语法的改变和新增的功能是非常重要的。通…

AlmaLinux download

前言 一个开源的、社区拥有和管理的、永远免费的企业级Linux发行版&#xff0c;专注于长期稳定性&#xff0c;提供一个健壮的生产级平台。AlmaLinux操作系统是1:1二进制兼容RHEL和pre-Stream CentOS。 AlmaLinux download VersionAlmaLinux downloadAlmaLinux repo阿里云百…

定时关机软件哪个好?定时关机软件大盘点

在生活和工作中&#xff0c;我们可以使用定时关机软件来定时关闭电脑&#xff0c;以实现对电脑的控制。那么&#xff0c;定时关机软件哪个好呢&#xff1f;下面我们就来了解一下。 定时关机软件的作用 定时关机软件可以帮助用户在预设的时间自动关闭电脑。这对于那些需要在特…

golang学习笔记——使用结构

使用结构 有时&#xff0c;你需要在一个结构中表示字段的集合。 例如&#xff0c;要编写工资核算程序时&#xff0c;需要使用员工数据结构。 在 Go 中&#xff0c;可使用结构将可能构成记录的不同字段组合在一起。 Go 中的结构也是一种数据结构&#xff0c;它可包含零个或多个…

springboot上传文件后显示权限不足

前言&#xff1a; 最近一个老项目迁移&#xff0c;原本一直好好的&#xff0c;迁移后上传文件的功能使用不正常&#xff0c;显示文件没有可读取权限&#xff0c;这个项目并不是我们开发和配置的&#xff0c;由第三方开发的&#xff0c;我们只是接手一下。 前端通过api上传文件…

笔记56:深度循环神经网络

本地笔记地址&#xff1a;D:\work_file\DeepLearning_Learning\03_个人笔记\3.循环神经网络\第9章&#xff1a;动手学深度学习~现代循环神经网络 a a a a a a a a

不会制作电子期刊怎么办?新发现

​电子期刊已经成为当今社会中非常流行的一种出版形式&#xff0c;它不仅方便快捷&#xff0c;而且易于分享和传播。如果你一直想尝试制作电子期刊&#xff0c;但又不知道如何开始&#xff0c;那么不用担心&#xff01;今天我将为你揭秘制作电子期刊的秘籍&#xff0c;让你轻松…

浅谈无线测温产品在菲律宾某工厂配电项目的应用

摘要&#xff1a;配电系统是由多种配电设备和配电设施所组成的变换电压和直接向终端用户分配电能的一个电力网络系统。由于配电系统作为电力系统的一个环节直接面向终端用户&#xff0c;它的完善与否直接关系着广大用户的用电可靠性和用电质量&#xff0c;因而在电力系统中具有…

linux高级篇基础理论五(用户安全,口令设置,JR暴力破解用户密码,NMAP端口扫描)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️不能因为人生的道路坎坷,就使自己的身躯变得弯曲;不能因为生活的历程漫长,就使求索的 脚步迟缓。 ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xff1a;云计算技…

HarmonyOS4.0系列——01、下载安装配置环境搭建以及运行Hello World

HarmonyOS4.0应用开发 安装编辑器 这里安装windows版本为例 安装依赖 打开DevEco Studio 这八项全部打钩即可开始编写代码&#xff0c;如果存在x&#xff0c;需要安装正确的库即可 开发 点击Create Project 选择默认模板——next Model部分分为Stage和FA两个应用模型&…

抖音运营的必备10个工具,开启智能拓客引流新时代!

先来看实操成果&#xff0c;↑↑需要的同学可看我名字↖↖↖↖↖&#xff0c;或评论888无偿分享 一、引言 亲爱的知友们&#xff0c;您们是否对抖音运营有浓厚的兴趣和独特的见解&#xff1f;今天&#xff0c;我将为您介绍一些抖音运营必备的工具&#xff0c;帮助您在抖音上脱颖…

10个Logo设计资源网站,绝对值得你收藏!

看似简单的标志背后的设计过程一点也不简单。优秀的标志个性鲜明&#xff0c;视觉冲击力强&#xff0c;易于识别和记忆。小标志使品牌的理念和形象一目了然地传达给消费者&#xff0c;使消费者产生良好的品牌联想&#xff0c;从而引导和促进消费。 在设计LOGO时&#xff0c;我…