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

文章目录

  • 前言
  • 一、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模型。

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

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

相关文章

[计网02] 数据链路层 笔记 总结 详解

目录 数据链路层概述 主要功能 封装成帧 透明传输 差错检测 冗余码 差错控制 检错编码 纠错编码 奇偶效验法 CRC循环冗余码 静态分配信道 频分多路复用FDM 时分多路复用TDM 波分多路复用WDM 码分多路复用CDM 随机访问介质的访问控制 ALOHA CSMA CSMA/CD CSMA/…

Weblogic Server工具WLST的使用

1.Weblogic脚本工具WLST介绍 可以用命令行来操作 Weblogic scripting tools 2.Weblogic WLST三种工作模式 2.1 wlst.sh tips:weblogic的T3 协议与HTTP/HTTPS 协议 操作如下&#xff1a;wlst在 common目录下 weblogic14c/wlserver/common/bin/ [weblogicfysedu32 weblogic]$…

【hadoop】解决浏览器不能访问Hadoop的50070、8088等端口?!

【hadoop】解决浏览器不能访问Hadoop的50070、8088等端口&#xff1f;&#xff01;&#x1f60e; 前言&#x1f64c;【hadoop】解决浏览器不能访问Hadoop的50070、8088等端口&#xff1f;&#xff01;查看自己的配置文件&#xff1a;最终成功访问如图所示&#xff1a; 总结撒花…

C# SQLite基础工具类

目录 1、安装System.Data.SQLite工具包 2、创建数据库 3、数据库的连接与断开 4、执行一条SQL语句 5、批量执行sql语句 6、返回首行首列值 7、执行sql语句返回datatable 1、安装System.Data.SQLite工具包 2、创建数据库 /// <summary> /// 数据库路径 …

深度学习模型压缩方法:知识蒸馏方法总结

本文将介绍深度学习模型压缩方法中的知识蒸馏,内容从知识蒸馏简介、知识的种类、蒸馏机制、师生网络结构、蒸馏算法以及蒸馏方法等六部部分展开。 一、知识蒸馏简介 知识蒸馏是指用教师模型来指导学生模型训练,通过蒸馏的方式让学生模型学习到教师模型的知识。在模型压缩中,…

在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…

VUE实现购物商城网站前端源码

文章目录 1.设计来源1.1 登录注册页面1.2 主界面1.3 列表界面1.4 详细界面1.5 购物车界面 2.源码2.1源码目录结构2.2源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/135054910 VUE实现购物商城网站前端源码&…

thinkphp的生命周期

1.入口文件 index.php 用户通过入口文件&#xff0c;发起服务请求&#xff0c;是整个应用的入口与七点 定义常量&#xff0c;加载引导文件&#xff0c;不要放任何业务处理代码 2.引导文件 start.php; 加载常量->加载环境变量->注册自动加载->注册错误与异常->加…

在modelsim中查看断言

方法一&#xff1a;单纯的modelsim环境 &#xff08;1&#xff09;编译verilog代码时按照system verilog进行编译 vlog -sv abc.v 或者使用通配符编译所有的.v或者.sv文件 &#xff08; vlog -sv *.sv *.v&#xff09; &#xff08;2&#xff09;仿真命令加一个-assert…

手把手教你制作微信图书小程序商城

随着微信小程序的普及和发展&#xff0c;越来越多的商家开始意识到微信小程序的商机。微信小程序商城成为了各行各业商家们开展线上业务的首选。那么&#xff0c;如何制作一款自己的微信图书小程序商城呢&#xff1f;下面就手把手教你一步步完成。 第一步&#xff0c;登录乔拓云…

前端路由模式

文章目录 一、hash 模式Hash 模式的特点window.onhashchange 事件 二、history 模式history APIwindow.onpopstate 事件解决history模式下页面刷新404问题 如何选择合适的路由模式 一、hash 模式 hash 模式是一种把前端路由的路径用 # 拼接在真实 url 后面的模式&#xff0c;通…

CentOS 7 制作openssh 9.6 rpm包更新修复安全漏洞 —— 筑梦之路

2023年12月18日 openssh 发布新版9.6p1&#xff0c;详细内容阅读OpenSSH: Release Notes 背景说明 之前也写过多篇制作openssh rpm包的文章&#xff0c;为何要重新来写一篇制作openssh 9.6版本的&#xff1f; openssh 9.6 rpm包制作和之前存在区别&#xff0c;对于CentOS 7来…

贝叶斯判别

参考文献&#xff1a; 6 判别分析 | 多元统计分析示例https://www.cnblogs.com/qizhou/p/13495598.html 一、问题描述 贝叶斯判别的本质是一类分类问题&#xff1a;基于若干采样样本&#xff0c;如何学习一个分类器对新样本数据进行分类并保证分类错误的概率最小。 假设 一…

java并发编程五 ReentrantLock,锁的活跃性

多把锁 一间大屋子有两个功能&#xff1a;睡觉、学习&#xff0c;互不相干。 现在小南要学习&#xff0c;小女要睡觉&#xff0c;但如果只用一间屋子&#xff08;一个对象锁&#xff09;的话&#xff0c;那么并发度很低 解决方法是准备多个房间&#xff08;多个对象锁&#xf…

Windows 11 系统下JDK17的下载与配置

一般地&#xff0c;在做Java项目的时候&#xff0c;我一般喜欢用 Idea 软件来写代码&#xff0c;idea是代码编辑器&#xff0c;JDK是用来翻译写好的代码&#xff0c;两者缺一不可&#xff01;&#xff01;&#xff01; 下面我就来讲一下如何进行JDK17的下载与配置 JDK17的下载…

鸿蒙ArkTS语言介绍与TS基础语法

1、ArkTS介绍 ArkTS是HarmonyOS主力应用开发语言&#xff0c;它在TS基础上&#xff0c;匹配ArkUI框架&#xff0c;扩展了声明式UI、状态管理等响应的能力&#xff0c;让开发者以更简洁、更自然的方式开发跨端应用。 JS 是一种属于网络的高级脚本语言&#xff0c;已经被广泛用…

联盟 | Shoplazza X HelpLook,AI技术助推商业效能增长

随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;其在各个领域的影响力日益增强。特别是近几年&#xff0c;无论是独立站还是电商平台&#xff0c;都在积极探索利用AI来分析数据以及提升运营效率的方式。 在这场AI技术的浪潮中&#xff0c;Shoplazza 与 Hel…

Jmeter关联操作

1.首先右键添加一个线程选择线程组,命名为线程组-1&#xff0c;添加取样器选择HTTP请求--城市天气 2.线程组-1右键&#xff0c;添加取样器选择后置处理器中的JSON提取器 3.线程组-1右键,添加取样器选择后置处理器中的BeanShell 后置处理程序(必须平级) 4.首先右键添加一个线程选…

JVM启动流程(JDK8)

JVM启动流程(JDK8) JVM的启动入口是位于jdk/src/share/bin/java.c的JLI_Launch函数,其定义如下: int JLI_Launch(int argc, char ** argv, /* main argc, argc */int jargc, const char** jargv, /* java args */int appclassc, const char** appclass…

贵金属入门知识有哪些?

贵金属作为国际知名的理财产品之一&#xff0c;市场人气居高不下&#xff0c;当前由于社会上的不确定分享增加&#xff0c;出于保值避险需求的推动&#xff0c;贵金属迎来了新一波发展高峰。对于刚刚接触贵金属市场的交易者来说&#xff0c;了解贵金属入门知识&#xff0c;做好…