IO多路转接 —— poll和epoll

news2024/11/25 14:53:46

文章目录

    • 1. poll
      • 1.1 poll的函数接口
      • 1.2 poll的简单测试程序
      • 1.3 poll的优缺点分析
    • 2. epoll
      • 2.1 epoll的函数接口
      • 2.2 epoll的工作原理
      • 2.3 epoll的工作模式(LT,ET)
      • 2.4 epoll的简易服务器实现(默认是LT工作模式)

前言: 接上文讲述的select,它有缺点,而且写起来复杂。于是就有了它的升级版,先出现了poll,到现在的epoll,逐渐进步,写起来方便了不少。poll用的不多,现在基本多路转接都是用的epoll,所以epoll要好好的讲讲,poll就当作选学内容,了解即可。


1. poll

poll与select相比,poll的编写较为简单。因为poll它有一个结构体,去管理关心事件,不需要像select还得自己去写第三方数组。其次poll它没有数量限制,select是数量限制的,但是数量大了必然会影响效率,poll和select在底层都是采用轮询检测的方式去查看要关心的fd的事件是否就绪,像epoll就不会采用这种轮询方式,这个后面讲。

总结:poll作为多路转接的一种方式,它是比select进步了的。

1.1 poll的函数接口

在这里插入图片描述

  • int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数参数:

  1. fds是一个结构体数组,每一个结构体都有它的fd,关心的事件,内核反馈事件。看一下这个结构体:
    struct pollfd {
    int fd; /* file descriptor /
    short events; /
    requested events /
    short revents; /
    returned events */
    };
  • events,就是用户告诉内核,它关心fd的事件。
  • revents,就是内核告诉用户,你关心的事件怎么样。
  • 其实这个events,revents还是位图,事件是宏定义好了的,可以看一下事件:
    在这里插入图片描述
    比如:我要设置结构体关心读事件,可以使得 events = POLLIN,如果还想要关心写事件,那么就是 events |= POLLOUT;怎么判断事件就绪呢?很简单,revents & POLLIN,为真就表示读事件就绪了。还是一些位操作。
  1. nfds 代表fds数组的长度. typedef unsigned long int nfds_t; 注意它的类型 nfds_t 其实就是一个 无符号long int。所谓fds数组长度,可以理解成fds数组中元素个数,可不敢理解成数组大小啊。
  2. timeout是表示poll函数的超时时间, 单位是毫秒(ms),这是一个时间线,在timeout之前是阻塞等待,超过timeout就是非阻塞等待(直接返回)。
    比如:你设置为 -1 ,那么就是永远的非阻塞式等待;设置为0,那就是永远的非阻塞式等待;或者你给个合理的时间线,控制一下,都可以。

函数的返回值:

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回.

1.2 poll的简单测试程序

了解函数接口后,就简单的写一个测试程序:

#include <iostream>
#include <poll.h>
#include <unistd.h>
int main()
{
    struct pollfd rfds;
    rfds.fd = 0;
    rfds.events = POLLIN;
    rfds.revents = 0;

    while (true)
    {
        int n = poll(&rfds, 1, -1);
        switch (n)
        {
        case -1:
            std::cerr << "poll errno" << std::endl;
            break;
        case 0:
            std::cout << "time out" << std::endl;
        default:
            std::cout << "有事件就绪" << std::endl;
            if (rfds.revents & POLLIN)
            {
                std::cout << "读事件就绪" << std::endl;
                char buffer[1024] = {0};
                ssize_t s = read(0, buffer, sizeof(buffer) - 1);

                if (s > 0)
                {
                    std::cout << "Say:" << buffer << std::endl;
                }
            }
            break;
        }
    }

    return 0;
}

可以看到poll的第三个参数我设置的是 -1 ,阻塞式等待。你当然可以改为0,看看现象,肯定是一刻不停的告诉你,time out

看一下运行结果:

在这里插入图片描述
我就怕小白事后问我,为啥你代码运行时,卡着不动。我告诉你哈,上面的代码检测的文件描述符是0,fd = 0是标准输入流,说人话 就是你 敲键盘 输入的东西,你不敲键盘输入,可不 它就 卡着不动,而且还是阻塞式等待 你输入。

1.3 poll的优缺点分析

  • 优点:
  1. poll的代码简单,select要操作位图,位图还得用专门的函数操作,并且还得自己写第三方数组。poll是用结构体数组,这个结构体就是pollfd,而且结构体里可以有fd信息,关心事件信息,内核反馈信息。非常不错。
  2. poll没有数量限制。

-缺点:

  1. poll和select一样,底层采用轮询方式检测就绪fd,所以数量一旦大了,就会效率降低
  2. poll 也要经常的从用户态到内核态,切换

2. epoll

epoll是对poll的又一进步,epoll它最关键的就是在底层不是用简单的轮询方式检测fd,并且对要监测的fd在底层通过红黑树进行高效的管理。这块在工作原理中详谈。

先看select、poll的缺点:

  • 1.每次调用时要重复地从用户态读入参数。
  • 2.每次调用时要重复地扫描文件描述符。
  • 3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。

总结就是:epoll它克服了这些缺点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法,epoll就是你告诉我要管理哪些事件,然后你就等我通知你 就可以了。

2.1 epoll的函数接口

在这里插入图片描述

  • int epoll_create(int size);

这个函数就是创建一个epoll模型,它的返回值是一个fd,也就是底层epoll的fd,后续的操作都是在这个epoll模型中,所以它的fd尤为重要。
创建成功返回fd,创建失败返回-1,并设置errno。
至于它的参数 size ,表示的就是epoll模型可以监测的fd的最大值。自从linux2.6.8之后,size参数是被忽略的。
注意: epoll模型创建成功后,它的返回值是一个fd,所以用完后,要记得close(fd)。

在这里插入图片描述

  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

这个函数就是用于管理epoll模型的:
函数参数:

  • epfd,epoll模型的fd
  • op,就是怎么操作,它可以有三个取值,也就是三个宏:
    EPOLL_CTL_ADD :注册新的fd到epfd中;
    EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL :从epfd中删除一个fd;
  • fd,需要监听的fd
  • event,这个是事件,告诉epoll模型要监听什么事。
    先来看看这个结构体:
    struct epoll_event
    {
    uint32_t events; /* Epoll events /
    epoll_data_t data; /
    User data variable */
    } __EPOLL_PACKED;
    可以看到,events就是事件,它也是宏:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.
可以看到epoll_event里面还有一个date,它也是一个结构体,最关键的就是它里面要保存fd。
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

在这里插入图片描述

  • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

这个函数就是用来取出内核反馈事件的:
函数参数:

  • epfd:epoll模型的fd
  • events:这个结构体组合就是用来存放,内核对监测fd的反馈情况的。
  • maxevents:这个参数要小于等于epoll_creat(size)的size,给成size就行。
  • timeout:和以往的IO模型一样,这是设置超时时间

返回值:返回值比较有意思,

  • 成功:返回对应I/O上已准备好的文件描述符数目,这个写代码的时候能感受到用处
  • 失败: 返回 -1,表明函数调用失败
  • 超时:返回值等于0

2.2 epoll的工作原理

了解epoll的工作原理,其实就是了解上面的三个函数接口,在底层都干了什么:

  • 调用epoll_create创建一个epoll模型;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪;

  1. epoll_create()
    调用epoll_create()后在内核会形成一个结构体eventpoll:
/* 
 * This structure is stored inside the "private_data" member of the file 
 * structure and rapresent the main data sructure for the eventpoll 
 * interface. 
 */  
struct eventpoll {  
    /* Protect the this structure access,可用于中断上下文 */  
    spinlock_t lock;  
    /* 
     * This mutex is used to ensure that files are not removed 
     * while epoll is using them. This is held during the event 
     * collection loop, the file cleanup path, the epoll file exit 
     * code and the ctl operations.用户进程上下文中 
     */  
    struct mutex mtx;  
    /* Wait queue used by sys_epoll_wait() */  
    wait_queue_head_t wq;  
    /* Wait queue used by file->poll() */  
    wait_queue_head_t poll_wait;  
    /* List of ready file descriptors */  
    struct list_head rdllist;  
    /* RB tree root used to store monitored fd structs */  
    struct rb_root rbr;  
    /* 
     * This is a single linked list that chains all the "struct epitem" that 
     * happened while transfering ready events to userspace w/out 
     * holding ->lock. 
     */  
    struct epitem *ovflist;  
    /* The user that created the eventpoll descriptor */  
    struct user_struct *user;  
};  

这个结构体维护了epoll模型的基本属性,其中最关键的两个就是:
struct rb_root rbrstruct list_head rdllist,这俩结构体大家很熟悉,第一个就是红黑树,第二个就是双向链表。

  • 红黑树是用于管理添加进来的事件的,也就是说你创建一个epoll模型,它是用红黑树来保存你向epoll模型中添加的事件。这样好处就是重复添加的事件就可以通过红黑树而高效的识别出来,而且红黑树查找的效率比轮询监测这种好多了。并且用户态调用 epoll_ctl()来操作 epoll 的监视文件时,需要增、删、改、查等动作有着比较高的效率。

  • 双向链表是用来保存已经准备就绪事件,以后查询就绪事件,就直接把双向链表的东西拷贝到上层,就是把就绪事件拿到了,并且就绪了n个事件,这个n就是链表的长度,也很方便。
    但是有个疑问:谁向双向链表中填入就绪事件呢?是内核直接填嘛?不是,有回调机制。

  • 回调机制:所有添加到epoll模型中的事件都会与设备(网卡)驱动程序建立回调方法,也就是说,当响应的事件发生时会调用这个回调方法.这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

  • 回调方法:并不是所有的文件都可以用epoll的,它必须有相应的回调方法:file_operations->poll,可以看一下:

struct file_operations {
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    int (*open) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    // 等
};

看到了把,这个回调方法是和底层硬件定制好了的,它有很多方法。也就是说,事件就绪好了,会通过回调机制(调用ep_poll_callback),去找对应的回调方法,然后再把就绪事件拷贝到双链表中,并且唤醒epoll模型,从而上层调用epoll_wait()可以拷贝双链表的事件。

大的逻辑已经有了,来看图解

在这里插入图片描述
这是epoll模型的大体样子,帮助理解用的。

  1. epoll_ctl()
    epoll_ctl()可以向epoll模型中添加事件,删除事件等操作。
    (1) 添加事件:epoll_ctl(int epfd,EPOLL_CTL_ADD , int fd, struct epoll_event *event)
    注意op选项设置为EPOLL_CTL_ADD。
    首先,它要添加一个事件到epoll模型,这个事件要被管理,用结构体 epitem管理:
struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}

注意这个epitem是事件在epoll模型中的管理的基本单位,把这个epitem要挂在红黑树里。红黑树的节点是一个键值对,键(key)就是fd,值(value)就是epitem。挂在红黑树上就完成了添加事件。
(2) 删除事件同理,就是红黑树上删除节点呗。
(3)修改事件,就是红黑树的节点的建不变(fd不变),修改它的epitem。

  1. epoll_wait()

那么等待文件描述符的事情,其实就是等待epoll模型中双向链表中是否有数据,有阻塞等,非阻塞等,设置timeout(三种方式)。但是我们得把基本等待就绪的过程,了解一下,这个就看图解吧:
在这里插入图片描述
假如有事件就绪了:

在这里插入图片描述
那么epoll_wait()被唤醒了,跑到内核去拷贝head_list中的事件,注意这个函数的返回值是已经准备就绪的事件的个数,就是链表的元素个数,这个效率是O(1)。

head_list中的元素就是事件,事件就是epitem结构体,在强调一遍。

2.3 epoll的工作模式(LT,ET)

工作模式有两种:

  • LT:关注的是双链表中是否有数据
  • ET:关注的是双链表中数据是否有变化

举个例子:
(1)你的快递一共有三个,假如是李四是快递员:

李四:你好,来取下快递。然后你去取快递,你只能拿走两件,并且你拿走了。李四发现你的快递还有遗留,立马通知你:你好,来取下快递。然后你又去取走了快递,这次你取走了最后一个快递。李四发现你的快递没存留,那么也不会通知你了。

(2)你的快递还是有三个,假如王五是快递员:

王五:你好,取一下快递。然后你去取快递,你只能拿走两件,并且你拿走了。但是你还剩下一件,王五不会管,他告诉过你 要来取快递了,取没取完 那是你的事。直到有一天,你又来了一件快递,王五发现你的快递来了,王五:你好,来取快递。你又来取快递,这次你长记性了,快一次就取完吧,不一次取完你都不通知我一下。王五说:只有你的快递从无到有,从少变多时,我才会通知你,换句话说,这有快递数发生变化,我才通知你。

其实李四对应的就是LT工作模式,王五对应的就是ET工作模式。非常有趣昂。

但是要使用ET模式,有一个非常重要的点:fd必须设置为非阻塞模式。

为什么ET模式下,fd必须是非阻塞呢?
当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对吧,这是好理解的。如果到socket编程中,这个问题会造成类似死锁的情况。

比如:
在这里插入图片描述
在这里插入图片描述
那么现在就有问题了,epoll_wait()在ET模式下,只有当缓冲区的数据发生变化,才会返回。但是客户端并没有再次发送数据,导致epoll_wait()不返回,一直等待。服务端,它只读取了1k,不完整报文,所有就等epoll_wait()返回,告诉它继续向下拿数据,但问题现在是epoll_wait()不返回。真的无语,现在的情况是,只有当客户端再次发送数据,epoll_wait()才能返回;但是客户端要等服务端的响应,才能继续发送数据。就是这种你等我,我等它,它等你的死循环。

怎么解决这种问题呢?毫无疑问,你必须循环的yici性把10k数据拿走,哪怕你一次拿1k,你循环的拿上10次,必须保证都拿完。这种保证的前提就是 fd是非阻塞的,它可以轮询的拿,而不是等待的拿。

对比LT和ET:

LT是 epoll 的默认行为。 使用 ET 能够减少 epoll 触发的次数, 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完,相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些.
但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了.

2.4 epoll的简易服务器实现(默认是LT工作模式)

到最后写一个epoll的简易代码,帮助理解。当然它是LT工作模式,如果想要挑战ET模式,那就下点狠功夫,之后我会写一个Reactor的小项目,其中就是利用epoll的ET模式,感兴趣的可以等后续。现在主要是先能把epoll的代码用起来,至于挑战,后面再说。

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>

using namespace std;

void useage()
{
    cout << "Please use"
         << "./epoll_server"
         << "+"
         << "端口号" << endl;
}

int main(int argv, char *argc[])
{
    启动服务器
    if (argv != 2)
    {
        useage();
        exit(1);
    }

    使服务器进入listen状态

    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0)
    {
        cerr << "listen failed" << endl;
        exit(2);
    }

    struct sockaddr_in my_sock;
    my_sock.sin_family = AF_INET;
    my_sock.sin_addr.s_addr = INADDR_ANY;
    my_sock.sin_port = htons(atoi(argc[1]));

    if (bind(listen_fd, (struct sockaddr *)&my_sock, sizeof(my_sock)) < 0)
    {
        cerr << "bind errno" << endl;
        exit(3);
    }

    if (listen(listen_fd, 5) < 0)
    {
        cerr << "listen errno" << endl;
        exit(4);
    }

    创建epoll模型,获取epfd

    int epfd = epoll_create(128);

     添加事件,这个就是先得添加listen_fd事件

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

     事件循环

    struct epoll_event revs[128]; // 这是用于接收返回fd情况的事件集合

    while (true)
    {
        int n = epoll_wait(epfd, revs, 128, -1);
        switch (n)
        {
        case -1:
            cerr << "epoll errno" << endl;
            break;
        case 0:
            cout << "time out" << endl;
            break;
        default:
            // 走到这里说明有事件就绪了
            cout << "有事件就绪" << endl;
            for (int i = 0; i < n; i++)
            {
                int sock = revs[i].data.fd;
                if (revs[i].events & EPOLLIN)
                {
                    cout << "文件描述符" << sock << "有读事件就绪" << endl;
                    if (sock == listen_fd)
                    {
                        cout << "*************************************" << endl;
                        cout << "有连接事件就绪" << endl;
                         在这里要处理连接事件
                        struct sockaddr_in sockaddr;
                        socklen_t j = sizeof(sockaddr);
                        int fd = accept(listen_fd, (struct sockaddr *)&sockaddr, &j);
                        if (fd < 0)
                        {
                            cerr << "accept errno" << endl;
                        }
                        cout << "连接成功" << endl;
                         连接完成后,要把fd以及它关心的事件,放到epoll模型中
                        struct epoll_event _ev;
                        _ev.data.fd = fd;
                        _ev.events = EPOLLIN;

                        if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev) == 0)
                        {
                            cout << "成功将" << fd << "托管给epoll" << endl;
                        }
                        else
                        {
                            cerr << "epoll_ctl failed" << endl;
                        }
                        cout << "*************************************" << endl;
                    }
                    else
                    {
                         走到这里说明是其他的普通fd的读事件就绪了
                        char buffer[1024] = {0};
                        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                        if (s > 0)
                        {
                            cout << sock << ":"
                                 << "正常数据读取" << endl;
                            buffer[s] = 0;
                            cout << "client:" << buffer << endl;
                            cout << "*************************************" << endl;
                        }
                        else if (s == 0)
                        {
                             说明对端连接关闭
                            cout << sock << ":"
                                 << "关闭连接" << endl;
                            close(sock);
                            // 记得管理epoll模型
                            epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                            std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                            cout << "*************************************" << endl;
                        }
                        else
                        {
                            // 读取失败
                            std::cout << "recv error" << std::endl;
                            close(sock);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
                            std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                            cout << "*************************************" << endl;
                        }
                    }
                }
            }
            break;
        }
    }

    close(epfd);
    close(listen_fd);

    return 0;
}

来看现象:
(1)服务器启动,并且客户端1连接:
在这里插入图片描述
(2)客户端2也去连接:

客户端使用telnet连接,
在这里插入图片描述
服务端现象:

在这里插入图片描述
(3)以上说明,连接的epoll管理没有问题,现在客户端1,发送消息:
在这里插入图片描述
(4)客户端2发送消息:
在这里插入图片描述
(5)以上说明接收数据epoll管理没有问题,那么客户端1退出:

在这里插入图片描述
以上测试都没问题,感兴趣的小伙伴,可以下去自己试一试。


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个UML的例子

一、需求分析 1.概念定义 需求分析&#xff0c;要明确以下4个概念的具体内容&#xff1a; 资源是指使用或产生的对象&#xff0c;如人、物料、信息、产品等。 目标是指希望资源处于什么样的状态。 过程是指被执行的活动&#xff0c;这些活动会改变资源的状态。 规则是指在某些…

2023-02-09 - 3 Elasticsearch基础操作

本章主要介绍ES的基础操作&#xff0c;具体包括索引、映射和文档的相关操作。其中&#xff0c;在文档操作中将分别介绍单条操作和批量操作。在生产实践中经常会通过程序对文档进行操作&#xff0c;因此在介绍文档操作时会分别介绍DSL请求形式和Java的高级REST编码形式。 1 索引…

VIF原理

文章目录一、VIF公式和原理对于R方一般回归模型皮尔逊相关系数中的方差VIF原理&#xff1a;一、VIF公式和原理 所谓VIF方法&#xff0c;计算难度并不高。在线性回归方法里&#xff0c;应用最广泛的就是最小二乘法&#xff08;OLS&#xff09;&#xff0c;只不过我们对每个因子…

锁的优化机制了解嘛?请进!

点个关注&#xff0c;必回关 文章目录自旋锁&#xff1a;自适应锁&#xff1a;锁消除&#xff1a;锁粗化&#xff1a;偏向锁&#xff1a;轻量级锁&#xff1a;从JDK1.6版本之后&#xff0c;synchronized本身也在不断优化锁的机制&#xff0c;有些情况下他并不会是一个很重量级的…

Open AI登录,可以使用ChatGPT!

目录 第一步&#xff1a;准备好上网工具 1、确认自己的IP 二、登录 三、无法登录的问题 第一步&#xff1a;准备好上网工具 1、确认自己的IP 先查看自己的电脑的IP&#xff0c;使用https://www.ip138.com/,看看&#xff0c;然后就可以将自己电脑的IP改成国外的IP&#x…

Word处理控件Aspose.Words功能演示:使用 C++ 拆分 MS Word 文档

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c;API支持所有流行的Word处理文件…

利用 ChatGPT 回答 R 相关问题

最近小编也尝试使用了 ChatGPT&#xff0c;下面给出一些知乎上常见 R 语言相关问题的回答。供大家参考&#xff1a; 目录 文章目录目录经典问题预测数据科学软件排名数据科学的 10 个常用 R 包R 语言入门书籍和网站推荐回答专业问题绘图绘制正负柱状图动态散点图构建 Shiny 程…

Java学习笔记-03(API阶段)

前言 目前我们看到的是Java基础部分的一个新的部分API,这是个啥,又能做啥呢? 其实可以概括成一句话:帮助我们站在巨人的肩膀上,实现更加高效的开发,那么我们来一探究竟吧~ API API&#xff08;Application Programming Interface&#xff0c;应用程序接口&#xff09;是一些预…

如何使用HTTP远程连接DOCKER?

/etc/systemd/system/docker.service.d/override.conf 写入文件 ##Add this to the file for the docker daemon to use different ExecStart parameters (more things can be added here) [Service] ExecStart ExecStart/usr/bin/dockerd默认情况下使用 systemd 时&#xff0…

报表生成器 FastReport .Net 用户指南 2023(十):Band的属性

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表&#xff0c;同时FastReport .Net支持中文、英语等14种语言&#xff0c;可以让你的产品保证真正的国际性。 FastReport.NET官方版…

HTTP协议基础知识(URL,请求响应格式,状态码,方法,cookie和session等)

目录 URL urlencode和urldecode http请求格式 http响应格式 http请求解析 http响应发送 http的方法 GET vs POST http的状态码 重定向 http常见报头属性 cookie和session URL 首先我们需要知道以下几点&#xff0c; 1.我们请求的图片&#xff0c;html&#xff0c;…

spring-webflux5 使用websocket

换做平常springboot程序中使用websocket的话是很简单的&#xff0c;只需要三步就能实现前后端的实时通讯。而在spring5中则更简单了&#xff0c;并且支持定点推送与全推送的灵活运用。在这里就分常规编程与响应式编程两种使用&#xff0c;进行记录下。一、非响应式编码1、引入W…

第十节 使用设备树插件实现RGB 灯驱动

Linux4.4 以后引入了动态设备树&#xff08;Dynamic DeviceTree&#xff09;&#xff0c;我们这里翻译为“设备树插件”。设备树插件可以理解为主设备树的“补丁”它动态的加载到系统中&#xff0c;并被内核识别。例如我们要在系统中增加RGB 驱动&#xff0c;那么我们可以针对R…

深入理解Android图形系统

一、图形系统简介图形系统是计算机中最重要的子系统之一。我们平时使用的电脑、手机都是图形界面的。对于普通人来说&#xff0c;没有图形界面的计算机几乎是没法用的&#xff0c;今天我们就来讲一讲图形系统背后的原理。1.1 图形系统的诞生早期的计算机是没有图形界面的&#…

一种基于深度学习的单导联心电信号睡眠呼吸暂停检测方法

在R峰识别的基础上&#xff0c;加入S峰的识别&#xff0c;并论正了该策略对检测结果的有效性。 1、大致方法 将数据集&#xff08;ECG信号&#xff09;划分为每五分钟的一个片段&#xff0c;为了减少噪声和信号伪影&#xff0c;首先对信号应用了一个有限脉冲响应&#xff08;…

使用python加密主机文件几种方法实现

本文主要介绍了使用python加密主机文件几种方法实现&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值&#xff0c;需要的朋友们下面随着小编来一起学习学习吧数据加密是一种保护数据安全的技术&#xff0c;通过对数据进行编…

你所不知道的Number()、parseInt()、parseFloat()转换细则

为什么要写这篇文章 最近在阅读《javascript高级程序设计》&#xff08;第6版&#xff09;这本书&#xff0c;以写代记。同时刚刚入职新公司&#xff0c;发现自己对于number类型转换的细节掌握不足 使用场景 我们常常对于很多字符串类型的数字需要转换成Number&#xff0c;一…

2023春节,蔚来高速免费换电累计达37万余次

2023年春节放假前&#xff0c;蔚来官方宣布高速换电免费消息&#xff1a; 在高速公路上&#xff08;含高速出入口&#xff09;的所有蔚来汽车换电站将为蔚来用户&#xff0c;提供不限次免费换电服务。 消息发布后&#xff0c;部分电动车企也进行了跟进&#xff0c;提供了特定区…

二十四、Gtk4-GtkExpression

GtkExpression是一种基本类型。它不是GObject的后代。GtkExpression提供了一种描述对值的引用的方法。GtkExpression需要求值才能获得值。 它类似于算术计算。 1 2 3 12是一个表达式。给出了计算的方法。3是来自表达式的值。求值是计算表达式并得到值。 GtkExpression是一种…

【大厂高频真题100题】《有效的井字游戏》 真题练习第26题 持续更新~

有效的井字游戏 给你一个字符串数组 board 表示井字游戏的棋盘。当且仅当在井字游戏过程中,棋盘有可能达到 board 所显示的状态时,才返回 true 。 井字游戏的棋盘是一个 3 x 3 数组,由字符 ,X 和 O 组成。字符 代表一个空位。 以下是井字游戏的规则: 玩家轮流将字符…