【网络编程】poll和epoll服务器的设计

news2024/12/23 18:10:09

文章目录

  • 前言
  • 一、poll
  • 二、epoll
    • 1.epoll初识
    • 2.epoll服务器的设计
    • 3.epoll的工作原理
    • 4.epoll的优点
    • 5.epoll的工作模式
  • 总结


前言

poll和select一样,也是一种linux中的多路转接的方案。而poll解决了select的两个问题:

1.select的文件描述符有上限的问题。

2.每次调用都要重新设置需要关心的文件描述符。


一、poll

首先我们认识一下poll的接口:

 第一个参数是一个结构体,我们可以将这个参数想象为一个new/malloc出来的动态数组,这个数组中每个元素的类型是一个结构体,结构体中有文件描述符,监听的事件集合,返回的事件集合三部分。

fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.

第二个参数是刚刚那个fds数组的长度。

第三个参数和我们学select中的timeval结构体差不多,时间单位是ms,当等于0时表示非阻塞等待,小于0表示阻塞式等待,大于0表示前面阻塞式等待,时间到了非阻塞返回一次。

这个函数的返回值和select一模一样,大于0表示有几个文件描述符的事件就绪了,等于0表示超时返回,小于0表示poll函数出现错误。

所以poll函数中,输入的时候看fd和events,输出的时候看fd和revents,通过将事件分为输出事件和返回事件来解决select中每次需要对文件描述符集重新设定的操作。

下面我们看看eventsrevents的取值都有哪些:

 虽然很多实际上我们能用到的很少,这些宏值实际上对应的在events和revents中的位图,只要我们设置在底层位图中就会将某个事件标记为1,我们讲几个常用的:POLLIN事件就是看哪些的文件描述符可以读了或者用户告诉操作系统帮我们关心某个文件描述符的读事件。POLLOUT事件就是看哪些的文件描述符可以写了或者用户告诉操作系统帮我们关心某个文件描述符的写事件。

POLLPRI与TCP的紧急指针相对应。

下面我们实现poll服务器:

首先我们创建poll所需要的struct pollfd类型数组,然后对数组初始化,这里我们可以写成扩容版数组,但是为了演示我们就用2018这个定值。

 void initServer()
        {
            _listensock = Sock::createSock();
            if (_listensock == -1)
            {
                logMessage(NORMAL,"createSock error");
                return;
            }
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
            _rfds = new struct pollfd[max_num];
            for (int i = 0;i<max_num;i++)
            {
                _rfds[i].fd = defaultfd;
                _rfds[i].events = 0;
                _rfds[i].revents = 0;
            }
            _rfds[0].fd = _listensock;
            _rfds[0].events = POLLIN;
        }

初始化服务器还是需要遍历数组将数组内的文件描述符设置为非法状态,并且事件初始化。然后我们将listensock放到数组第一个位置并且让系统帮我们监视listen文件描述符的读事件。

void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = poll(_rfds,max_num,timeout);
                switch (n)
                {
                    case 0:
                        logMessage(NORMAL,"time out.....");
                        break;
                    case -1:
                        logMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        //说明有事件就绪了
                        //logMessage(NORMAL,"get a new link");
                        HanderEvent();
                        break;
                }
            }
        }

启动的时候我们不需要再像select那样每次先重新设置文件描述符集并且把数组中合法fd读到文件描述符集中并且select还需要知道最大文件描述符,这些在poll中统统不要,只需要将数组传进去系统会帮我们管理,我们设置的时间为-1表示阻塞式监视。

 void HanderEvent()
        {
            for (int i = 0;i<max_num;i++)
            {
                //过滤掉非法的文件描述符
                if (_rfds[i].fd == defaultfd) 
                    continue;
                //过滤掉没有设置读事件的文件描述符
                if (!(_rfds[i].events & POLLIN)) 
                    continue;
                //如果是listensock事件就绪,就去监听新连接获取文件描述符,如果不是listensock事件,那么就是普通的IO事件就绪了 
                if (_rfds[i].fd == _listensock && (_rfds[i].revents & POLLIN))
                {
                    Accepter(_listensock);
                }
                else if ((_rfds[i].revents & POLLIN))
                {
                    Recver(i);
                }
                else 
                {

                }
            }
        }

在hander函数中,因为我们目前演示的服务器只涉及读数据,所以我们先过滤数组中非法的文件描述符,然后再过滤不关心读事件的文件描述符,然后判断listensock文件描述符的读事件是否就绪,如果就绪了就去执行监听新连接的函数,如果是其他文件描述符的读事件就绪,那么就进行数据读取和发送响应的函数。

void Accepter(int listensock)
        {
            // listensock必然就绪
            std::string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accept(listensock, &clientip, clientport);
            if (sock < 0)
            {
                return;
            }
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
            // 开始进行服务器的处理逻辑
            // 将accept返回的文件描述符放到自己管理的数组中,本质就是放到了select管理的位图中
            int i = 0;
            for (i = 0; i < max_num; i++)
            {
                if (_rfds[i].fd != defaultfd)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            if (i == max_num)
            {
                logMessage(WARNING, "server is full ,please wait");
                close(sock);
            }
            else
            {
                _rfds[i].fd = sock;
                _rfds[i].events = POLLIN;
                _rfds[i].revents = 0;
            }
            print();
        }
        void Recver(int pos)
        {
            //注意:这样的读取有问题,由于没有定协议所以我们不能确定是否能读取一个完整的报文,并且还有序列化反序列化操作...
            //由于我们只做演示所以不再定协议,在TCP服务器定制的协议大家可以看看
            char buffer[1024];
            ssize_t s = recv(_rfds[pos].fd,buffer,sizeof(buffer)-1,0);
            if (s>0)
            {
                buffer[s] = 0;
                logMessage(NORMAL,"client# %s",buffer);
            }
            else if (s == 0)
            {
                //对方关闭文件描述符,我们也要关闭并且下次不让select关心这个文件描述符了
                close(_rfds[pos].fd);
                _rfds[pos].fd = defaultfd;
                _rfds[pos].events = 0;
                _rfds[pos].revents = 0;
                logMessage(NORMAL,"client quit");
            }
            else 
            {
                //读取失败,关闭文件描述符
                close(_rfds[pos].fd);
                _rfds[pos].fd = defaultfd;
                _rfds[pos].events = 0;
                _rfds[pos].revents = 0;
                logMessage(ERROR,"client quit: %s",strerror(errno));
            }
            //2.处理 request
            std::string response = func(buffer);

            //3.返回response
            write(_rfds[pos].fd,response.c_str(),response.size());
        }

这两个函数只需要记得当读取失败的时候需要重置数组中文件描述符结构体。

下面我们运行起来:

可以看到也是没问题的。 

poll的优缺点:

poll 的优点:
不同与 select 使用三个位图来表示三个 fdset 的方式, poll 使用一个 pollfd 的指针实现 .
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比
select更方便.
poll并没有最大数量限制 (但是数量过大后性能也是会下降)
poll 的缺点:
poll 中监听的文件描述符数目增多时:
和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

二、epoll

1.epoll初识:

epoll可以理解为是增强版的poll,按照man手册的说法:epoll是为了处理大批量句柄而作了改进的poll.

epoll有三个系统调用:

epoll_create:
int epoll_create(int size);
创建一个 epoll 的句柄 .
自从linux2.6.8之后,size参数是被忽略的,但是我们必须传一个大于0的数字
用完之后, 必须调用close()关闭

如果创建成功了会给我们返回一个文件描述符,如果失败返回-1。实际上epoll_create就是创建一个epoll模型。

epoll_ctl:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll 的事件注册函数 .
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示,分别是增加,修改,删除。比如我们想让操作系统帮我们新增一个需要检视的文件描述符,这个时候选项就是增加的选项。

第二个参数的取值:

EPOLL_CTL_ADD :注册新的fd到epfd中;

EPOLL_CTL_MOD :修改已经注册的fd的监听事件;

EPOLL_CTL_DEL :从epfd中删除一个fd
如下图:

 第三个参数是需要监听的fd.

第四个参数是告诉内核需要监听什么事,与poll中struct pollfd类似。下面我们看看epoll _event的结构:

 

 如上图:uint32_t events就是宏的集合,如果文件描述符是读事件那么就会设置为EPOLLIN,data由用户定义,可以是指针可以是文件描述符等。

如果函数成功则返回0,否则返回-1.

epoll_wait:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll 监控的事件中已经发送的事件 .
参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
此函数的返回值和select/poll一模一样,0表示超时,小于0表示错误,大于0表示有几个文件描述符事件就绪。

2.epoll服务器的设计

首先作为一款服务器,端口号肯定是必须要有的,并且我们还需要监听套接字和epoll对象模型,这个epoll对象模型我们讲过,实际上就是一个int变量。

void initServer()
        {
            //1.创建socket
            _listensock = Sock::createSock();
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
            //2.创建epoll模型
            _epfd = epoll_create(size);
            if (_epfd < 0)
            {
                logMessage(FATAL,"epoll create error: %s",strerror(errno));
                exit(EPOLL_CREATE_ERR);
            }
            //3.添加listensock到epoll中
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = _listensock;  //当事件就绪被重新捞取上来的时候我们要知道是哪一个fd就绪了
            epoll_ctl(_epfd,EPOLL_CTL_ADD,_listensock,&ev);
            //4.申请就绪事件的空间
            _revs = new struct epoll_event[_num];
            logMessage(NORMAL,"init server success");
        }

我们在初始化服务器的时候首先创建套接字然后创建epoll模型(创建epoll模型的参数只要大于0即可,我们添加一个变量size用来使用),然后我们需要将监听套接字添加到epoll中,epoll_ctl的参数我们已经讲过了,第一个就是epoll创建成功返回的句柄,第二个参数是选项可以是添加也可以是删除或者修改,第三个参数就是要添加的文件描述符,第四个参数是对此文件描述符事件的设置,要设置监听套接字的读事件,首先创建一个struct epoll_event对象,然后把事件设置为读事件,把监听套接字放入data中的fd,这一步操作非常重要,因为我们后续事件就绪捞出事件时需要知道这个事件是哪个文件描述符。设置成功后我们还需要创建一个struct epoll_event的数组(这个数组用来存放所有的struct epoll_event),有了这个数组后我们就可以开辟空间了,这里的空间大家可以改为扩容版,我们为了演示就用了固定大小:

void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = epoll_wait(_epfd,_revs,_num,timeout);
                switch (n)
                {
                    case 0:
                        logMessage(NORMAL,"timeout......");
                        break;
                    case -1:
                        logMessage(WARNING,"epoll_wait error: %s",strerror(errno));
                        break;
                    default:
                        logMessage(NORMAL,"have event ready");
                        HanderEvent(n);
                        break;
                }
            }
        }

 我们启动服务器的时候就进行epoll_wait,这个函数与select和epoll的返回值一模一样,第一个参数是epoll创建的句柄,第二个参数是struct epoll_events数组,这个数组epoll会帮我们管理,数组大小就是我们前面定义的num,时间设置为阻塞式。当返回值大于0说明有几个事件就绪了,我们就去调用事件处理函数:

 void HanderEvent(int readynum)
        {
            for (int i = 0;i<readynum;i++)
            {
                int sock = _revs[i].data.fd;
                uint32_t events = _revs[i].events;
                if (sock == _listensock && (events & EPOLLIN))
                {
                    //listen读事件就绪,获取新连接
                    std::string clientip;
                    uint16_t clientport;
                    int fd = Sock::Accept(_listensock,&clientip,clientport);
                    if (fd<0)
                    {
                       logMessage(WARNING,"accept error"); 
                       continue;
                    }
                    //获取fd成功不可以直接读取,要放入epoll
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    epoll_ctl(_epfd,EPOLL_CTL_ADD,fd,&ev);
                     
                }
                else if ((events & EPOLLIN))
                {
                    char buffer[1024];
                    //普通读事件就绪
                    int n = recv(sock,buffer,sizeof(buffer)-1,0);
                    if (n>0)
                    {
                        buffer[n] = 0;
                        logMessage(NORMAL,"client# %s",buffer); 
                        std::string response = _func(buffer);
                        send(sock,response.c_str(),response.size(),0);
                    }
                    else if (n==0)
                    {
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(NORMAL,"client quit"); 
                    }
                    else 
                    {
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(ERROR,"recv error: %s",strerror(errno)); 
                    }
                }
                else 
                {

                }
            }
        }

首先遍历事件数组,然后记录每个结构体的fd和事件,当是监听套接字并且设置了读事件,那么我们就监听新连接,监听成功后要将用于通信的套接字设置进epoll中。如果不是监听套接字并且设置了读事件那么就是普通事件了,我们需要读数据,注意:我们在select,poll,epoll中演示的读取和发送都是有问题的,不仅仅要保证数据是一个完整的报文,而且还得考虑一次读完数据或者一次读不完数据的情况,并且还有序列化和反序列化等。我们只是演示多路转接对于事件的推送机制。

数据读成功后我们就发送,如果客户端关闭了文件描述符或者recv函数失败,则先将此文件描述符在epoll中删除,然后再关闭文件描述符。

下面我们运行起来:

 可以看到是没问题的,这就是我们epoll服务器的测试代码。

3.epoll的工作原理

首先epoll的底层有红黑树和双向链表,当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关:

struct eventpoll{ 
 .... 
 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
 struct rb_root rbr; 
 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
 struct list_head rdlist; 
 .... 
};

 红黑树用来存储所有添加到epoll中的需要监控的事件,而双向链表的作用是当用户调用epoll_wait时给用户返回事件就绪的节点。

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
在epoll中,对于每一个事件,都会建立一个epitem结构体。
struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
总结一下, epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪

4.epoll的优点

epoll 的优点 select 的缺点对应
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开来。
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限

注意:有种说法是:epoll中使用了内存映射机制

内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销。
这种说法是不准确的 . 我们定义的 struct epoll_event 是我们在用户空间中分配好的内存 . 势必还是需要将内核的数据拷贝到这个用户空间的内存中的。

5.epoll的工作模式

例子:

我们已经把一个tcp socket添加到epoll描述符,这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作,然后调用read, 只读取了1KB的数据
继续调用epoll_wait......

1.LT(水平触发)

epoll 默认状态下就是 LT 工作模式 .
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait
仍然会立刻返回并通知socket读事件就绪.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写。
我们之前的select和poll都是水平触发模式,也就是说如果你没将就绪的事件中的数据处理完就会一直提醒你事件就绪。

2.ET(边缘触发)

如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志 , epoll 进入 ET 工作模式 .
当epoll检测到socket上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
epoll_wait 不会再返回没读完的事件就绪.
也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
只支持非阻塞的读写
select poll 其实也是工作在 LT 模式下 . epoll 既可以支持 LT, 也可以支持 ET
对于ET模式,如果通知你一次没有处理数据,那么只有当数据增加或减少时ET才会再通知你一次。
3. 对比 LT ET
LT epoll 的默认行为 . 使用 ET 能够减少 epoll 触发的次数 . 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后 , 不会反复被提示就绪 , 看起来就比 LT 更高效一些 . 但是在 LT 情况下如果也能做到
每次就绪的文件描述符都立刻处理 , 不让这个就绪被重复提示的话 , 其实性能也是一样的 .
另一方面 , ET 的代码复杂程度更高了。
4.理解 ET 模式和非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践" 上的要求.
假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求。
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来, 参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中。此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回。 但是问题来了:只有服务器全部读取完数据才会给客户端发送确认应答,客户端收到服务器全部读取完毕的确认应答后才会发送新的数据,这就产生了鸡生蛋,蛋生鸡的问题。
所以 , 为了解决上述问题 ( 阻塞 read 不一定能一下把完整的请求读完 ), 于是就可以使用非阻塞轮训的方式来读缓冲区 , 保证一定能把完整的请求都读出来.
而如果是 LT 没这个问题 . 只要缓冲区中的数据没读完 , 就能够让 epoll_wait 返回文件描述符读就绪
当然我们也可以用一个更简单的方式理解:首先ET模式只通知一次,所以这就强迫程序员必须一次将数据全部取走,而要想一次取走全部数据就必须要循环式读取,只有循环式读取读取不到数据了才说明把缓冲区的数据取完了,如果这个时候我们的文件描述符是阻塞式读取,那么一旦读取不到数据了就会阻塞住(阻塞式读取如果没有数据则阻塞等待数据就绪),一旦阻塞就完蛋了,因为多乱转接是单进程的,这就导致整个进程都阻塞住了,所以为了解决这个问题必须将文件描述符设置为非阻塞读取。(还记得我们以前是如何保证读取完数据的吗?我们当时定制了协议,数据前面有数据长度的字段,循环读取数据一旦读取的数据大小是一个完整报文的长度我们就会退出循环)
5. epoll 的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型。

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

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

相关文章

05鸿蒙APP开发之加载网络列表

目录 1、概述2、http请求封装2.1、为什么要封装Http请求&#xff1f;2.2、封装后的网络请求 3、发起请求并渲染列表数据第一步&#xff1a;准备一个目前能用的&#xff0c;测试API地址&#xff0c;如下&#xff1a;第二步&#xff1a;创建对应的实体对象第三步&#xff1a;调用…

10 Vue3中v-html指令的用法

概述 v-html主要是用来渲染富文本内容&#xff0c;比如评论信息&#xff0c;新闻信息&#xff0c;文章信息等。 v-html是一个特别不安全的指令&#xff0c;因为它会将文本以HTML的显示进行渲染&#xff0c;一旦文本里面包含一些恶意的js代码&#xff0c;可能会导致整个网页发…

对大学生创新创业某赛事目前存在的烂尾楼现象的一些研究的分享(1)

经过对”某某网”大学生创新创业大赛国赛第五届-第八届部分金奖项目的研究&#xff0c;进行较为充分的信息溯源、穿透调查&#xff0c;我发现不少项目存在赛事材料画大饼&#xff0c;严重不切合实际&#xff0c;参赛人员并非真正创新创业&#xff0c;赛后迅速销声匿迹、烂尾切割…

2023年全球架构师峰会(ArchSummit北京站2023)-核心PPT资料下载

一、峰会简介 ArchSummit聚焦业界强大的技术成果&#xff0c;秉承“实践第一、案例为主”的原则&#xff0c;展示先进技术在行业中的典型实践&#xff0c;以及技术在企业转型、发展中的推动作用。旨在帮助技术管理者、CTO、架构师做好技术选型、技术团队组建与管理&#xff0c…

机器学习——分类评价指标

【说明】文章内容来自《机器学习——基于sklearn》&#xff0c;用于学习记录。若有争议联系删除。 1、评价指标 对于模型的评价往往会使用损失函数和评价指标&#xff0c;两者的本质是一致的。一般情况下&#xff0c;损失函数应用于训练过程&#xff0c;而评价指标应用于测试过…

Apache Superset如何实现无公网ip实时远程访问本地数据【内网穿透】

文章目录 前言1. 使用Docker部署Apache Superset1.1 第一步安装docker 、docker compose1.2 克隆superset代码到本地并使用docker compose启动 2. 安装cpolar内网穿透&#xff0c;实现公网访问3. 设置固定连接公网地址 前言 Superset是一款由中国知名科技公司开源的“现代化的…

CloudCanal x Debezium 打造实时数据流动新范式

简述 Debezium 是一个开源的数据订阅工具&#xff0c;主要功能为捕获数据库变更事件发送到 Kafka。 CloudCanal 近期实现了从 Kafka 消费 Debezium 格式数据&#xff0c;将其 同步到 StarRocks、Doris、Elasticsearch、MongoDB、ClickHouse 等 12 种数据库和数仓&#xff0c;…

[linux]进程间通信-管道pipe的实际用法(写入/读取)

一、需求 现有两个进程A和B&#xff0c;B进程含较为独立且复杂的业务逻辑&#xff0c;A进程为主控进程&#xff0c;现A进程需要控制B进程执行对应的功能&#xff0c;且要保持响应及时。 二、分析 典型进程间通信案例&#xff0c;因此使用linux下的管道方法&#xff08;pipe&…

大创项目推荐 深度学习 机器视觉 人脸识别系统 - opencv python

文章目录 0 前言1 机器学习-人脸识别过程人脸检测人脸对其人脸特征向量化人脸识别 2 深度学习-人脸识别过程人脸检测人脸识别Metric Larning 3 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习 机器视觉 人脸识别系统 该项目…

在RT-Thread中使用SystemView进行调试分析

一、SystemView SystemView is a toolkit for visual analysis of any embedded system. SystemView gives complete insight into an application, to gain a deep understanding of the runtime behavior, going far beyond what a debugger is offering. This is particula…

maven学习和maven聚合工程搭建

1.学习maven maven的概念 项目管理工具 &#xff0c;对jar进行依赖管理&#xff0c;编译&#xff0c;打包&#xff0c;单元测试&#xff0c;安装&#xff0c;部署&#xff0c;贯穿整个项目 为什么要学maven 要解决的问题&#xff1a; 不同的开发工具开发出来的项目目录结构…

元旦厦门游--ChatGPT/GPT4科研实践应用与AI绘图技术

2023年我们进入了AI2.0时代。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车&#xff0c;就有可能被淘汰在这个数字化时代&#xff0c;如何能高效地处理文本、文献查阅、PPT…

马蹄集第37周

1、最小子串覆盖 考点&#xff1a;双指针 代码&#xff1a; def judge(s,t,l,r):m [0] * (256)for i in t:m[ord(i)] 1for i in range(l,r1):if s[i] in t:m[ord(s[i])] - 1for i in m:if i > 0:return Falsereturn Truedef main():result "No"s input()t i…

EMNLP 2023 亮点回顾:大模型时代下的 NLP 研究

作为自然语言处理&#xff08;NLP&#xff09;领域的顶级盛会&#xff0c;EMNLP 每年都成为全球研究者的关注焦点。2023 年的会议在新加坡举行&#xff0c;聚集了数千名来自世界各地的专家学者&#xff0c;也是自疫情解禁以来&#xff0c;中国学者参会最多的一次。巧的是&#…

windows 快捷键 win+tab 图标显示工具栏设置

在Windows中&#xff0c;按下Win Tab组合键会打开任务视图&#xff0c;该视图显示当前打开的窗口以及工作区的虚拟桌面。如果你希望在任务视图中显示工具栏设置&#xff0c;可以按照以下步骤进行&#xff1a; 1. 打开设置&#xff1a; 使用快捷键Win I或在任务栏上右键单击开…

南京大学-软件分析-课程05-数据流分析基础理论2

7. 迭代算法与不动点定理关联 我们需要证明自己的Transfer Function是单调的 代码什么时候会停止 8. May/Must Analysis, A Lattice View May Analysis Unsafe result->Safe result 如果我是一个查错的定义&#xff0c; 没有no definition的错误&#xff0c;这是一个不安…

Java开发框架和中间件面试题(2)

8.说说自己对Spring MVC的了解&#xff1f; MVC是一种设计模式&#xff0c;Spring MVC是一款很优秀的MVC框架。Spring MVC可以帮助我们进行更简洁的Web层开发&#xff0c;并且它天生与Spring框架集成。SpringMVC下我们一般把后端项目分为Service&#xff08;处理业务&#xff0…

21、Web攻防——JavaWeb项目JWT身份攻击组件安全访问控制

文章目录 一、JavaWeb二、JWT攻击 一、JavaWeb webgoat 1.java web的配置文件&#xff0c;配置文件一般在META-INF目录下&#xff0c;文件名常为pom.xml或web.xml 2.如何通过请求&#xff0c;查看运行的java代码。 地址信息PathTraversal/profile-upload 直接找到以该字符P…

【网络技术设备安全】BGP 基础与概述-2-中转 AS 中的 IBGP 路由传递

0x01 中转 AS 中的 IBGP 路由传递 参考该图&#xff1a; 上图&#xff0c;我们模拟一个 1.0 的路由通过 AS 65101 来传递 1&#xff1a;通过图可知&#xff0c;A 与 B 之间的 Peer 为 EBGP&#xff0c;B 与 E 之间为 Peer IBGP&#xff0c;E 与 F 之间为 Peer EBGP 邻接 2&a…