IO 多路转接之 epoll

文章目录

  • IO 多路转接之 epoll
    • 1、IO 多路转接之 poll
      • 1.1、poll 函数
      • 1.2、poll 函数返回值
      • 1.3、Socket 就绪条件
        • 1.3.1、读就绪
        • 1.3.2、写就绪
        • 1.3.3、异常就绪
      • 1.4、poll 的优点
      • 1.5、poll 的缺点
      • 1.6、poll 改写 select
    • 2、IO 多路转接之 epoll
      • 2.1、epoll 函数
      • 2.2、epoll_create
      • 2.3、epoll_ctl
      • 2.4、epoll_wait
      • 2.5、epoll 工作原理
      • 2.6、水平触发 Level Triggered 工作模式(默认模式 LT)
      • 2.7、边缘触发 Edge Triggered 工作模式
      • 2.8、LT 和 ET 对比
      • 2.9、理解 ET 模式和非阻塞文件描述符
      • 2.10、epoll 使用场景
      • 2.11、epoll 示例
        • 2.11、LT 模式的 epoll 服务器响应程序
        • 2.12、ET 模式的 epoll 服务器响应程序

img

IO 多路转接之 epoll

1、IO 多路转接之 poll

1.1、poll 函数

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  • fds:fds 是一个 poll 函数监听的结构列表,每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合(监听到的)。
struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};
  • nfds:监控的文件描述符个数

  • timeout:单位是毫秒

其中,events 和 revents 的取值可以是下面表格中的值:

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux 不支持)
POLLPRI高优先级数据可读,例如 TCP 带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP 连接被对方关闭,或者对方关闭了写操作。它由 GNU 引入
POLLERR错误
POLLHUP挂起。比如管道的写端关闭后,该端描述符上将收到 POLLHUP 事件
POLLNVAL文件描述符没有打开

1.2、poll 函数返回值

  • >0表示等待某些事件成功。
  • =0表示超时。
  • <0表示错误。错误信息会设置在 errno 中,错误情况类别和 select 一样。

1.3、Socket 就绪条件

1.3.1、读就绪
  • socket 内核中,接收缓冲区中的字节数,大于等于低水位标记 SO_RCVLOWAT (有足够的数据可以读的意思吧),此时可以无阻塞的读该文件描述符,并且返回值大于 0
  • socket TCP 通信中,对端关闭连接,此时对该 socket 读,则返回 0
  • 监听的 socket 上有新的连接请求
  • socket 上有未处理的错误

1.3.2、写就绪
  • socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于 0

  • socket 的写操作被关闭(close 或者 shutdown),对一个写操作被关闭的 socket进行写操作, 会触发 SIGPIPE 信号

  • socket 使用非阻塞 connect 连接成功或失败之后

  • socket 上有未读取的错误


1.3.3、异常就绪

socket 上收到带外数据。

关于带外数据,和 TCP 紧急模式相关(回忆 TCP 协议头中,有一个紧急指针的字段)。

这个数据需要紧急处理。


1.4、poll 的优点

不同于 select 使用三个位图来表示三个 fd_set 的方式,poll 使用一个 pollfd 的指针实现。

  • pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方式
  • 接口使用比 select 更方便
  • poll 并没有最大数量限制 (但是数量过大后性能也是会下降)

1.5、poll 的缺点

poll 中监听的文件描述符数目增多时:

  • 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符
  • 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降

1.6、poll 改写 select

前面有提到 select 的缺点:

  • 文件描述符限制select 有一个限制,即它最多只能监控 1024 个文件描述符(在一些系统中,可以通过重新编译内核修改这个值)。这是因为 select 使用的是一个固定大小的位掩码来表示文件描述符。

  • 静态数组:文件描述符通过 fd_set 结构表示,这个结构是一个固定大小的位数组,每次调用 select 后需要重新设置。

  • 效率低下:随着监控的文件描述符增多,select 需要遍历整个集合来检查状态变化,性能较差。尤其是当大量文件描述符中只有少数活跃时,这种线性扫描的方式会非常低效。

poll 对 select 进行了改进。

poll 对 select 的主要改进总结:

  1. 文件描述符限制poll 取消了 select 中的 1024 文件描述符限制,可以处理任意数量的文件描述符。
  2. 无需重置集合:在 select 中,每次调用后都需要重置 fd_set,而在 poll 中不需要这样做。
  3. 性能提升poll 在实现上更为高效,特别是文件描述符数量较多的情况下,poll 的结构体数组相比 select 的位掩码结构更加灵活,减少了不必要的遍历。

改进后的主要文件代码:SelectServer.hpp->PollServer.hpp

#pragma once#include <iostream>
#include <memory>
#include <poll.h>
#include <sys/time.h>
#include <string>
#include "Socket.hpp"using namespace socket_ns;class PollServer
{const static int defaultfd = -1;const static int N = 1024;const static int timeout = -1; // 负数阻塞式等待,整数等待的毫秒值public:PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()){InetAddr client("0", port);_listensock->BuildListenSocket(client);for (int i = 0; i < N; ++i){_fds[i].fd = defaultfd;_fds[i].revents = 0;_fds[i].events = 0;}_fds[0].fd = _listensock->SockFd(); // 第一个肯定是listensock的文件描述符_fds[0].events = POLLIN;            // 对读事件关心}void AcceptClient(){InetAddr clientaddr;socket_sptr sockefd = _listensock->Accepter(&clientaddr);int fd = sockefd->SockFd();if (fd >= 0){LOG(DEBUG, "Get new Link ,sockefd is :%d ,client info : %s:%d", fd, clientaddr.Ip().c_str(), clientaddr.Port());}// 把新到的文件描述符交给select托管,使用辅助数组int pos = 1;for (; pos < N; pos++){if (_fds[pos].fd == defaultfd)break;}if (pos == N){// 满了::close(fd); // 这里就是比select更好,可以扩容,也可以直接关闭文件描述符LOG(WARNING, "server full ...");return;}else{_fds[pos].fd = fd; // 添加新文件描述符_fds[pos].events = POLLIN;// 对读事件关心_fds[pos].revents = 0;LOG(WARNING, "%d sockfd add to select array", fd);}LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}void ServiceIO(int pos){char buff[1024];ssize_t n = ::recv(_fds[pos].fd, buff, sizeof(buff) - 1, 0);if (n > 0){buff[n] = 0;LOG(DEBUG, "client # %s", buff);std::string message = "Server Echo# ";message += buff;::send(_fds[pos].fd, message.c_str(), message.size(), 0);}else if (n == 0){LOG(DEBUG, "%d socket closed!", _fds[pos].fd);::close(_fds[pos].fd); // 有用户退出,把该文件描述符重置为默认值_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}else{LOG(DEBUG, "%d recv error!", _fds[pos].fd);::close(_fds[pos].fd);_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}}void HandlerRead(){for (int i = 0; i < N; i++){if (_fds[i].fd == defaultfd)continue;if (_fds[i].revents & POLLIN) // 读事件就绪{if (_fds[i].fd == _listensock->SockFd()) // listensock{AcceptClient();}else // 真正的读事件就绪{// socket读事件就绪ServiceIO(i);}}else if(_fds[i].revents & POLLOUT){// 写事件就绪,后面epoll再做}}}void Loop(){while (true){int n = poll(_fds, N, timeout);if (n > 0){// 处理读文件描述符HandlerRead();}else if (n == 0){//  时间到了LOG(DEBUG, "time out ...");}else{// 错误LOG(FATAL, "select error ...");}}}std::string RfdsToStr(){std::string rfdstr;for (int i = 0; i < N; ++i){if (_fds[i].fd != defaultfd){rfdstr += std::to_string(_fds[i].fd);rfdstr += " ";}}return rfdstr;}~PollServer() {}private:uint16_t _port;std::unique_ptr<TcpSocket> _listensock;struct pollfd _fds[N]; // 可以设置成容量满自动扩容模式
};

poll 服务器响应程序整体代码

尽管 poll 在很多方面改进了 select,但它在某些场景下依然存在效率问题(例如大量空闲文件描述符时依然需要线性扫描)。为此,Linux 后来引入了更高效的机制,例如 epoll


2、IO 多路转接之 epoll

2.1、epoll 函数

按照 man 手册的说法:是为处理大批量句柄而作了改进的 poll。

它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法。

The  epoll  API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them.   The  epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors.

下面介绍 epoll 相关的系统调用。


2.2、epoll_create

#include <sys/epoll.h>int epoll_create(int size);

创建一个 epoll 的句柄

自从 linux2.6.8 之后,size 参数是被忽略的

用完之后, 必须调用 close()关闭


2.3、epoll_ctl

#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);

epoll 的事件注册函数

它不同于 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型

  • 第一个参数是 epoll_create()的返回值(epoll 的句柄)

  • 第二个参数表示动作,用三个宏来表示(EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL)

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

  • 第四个参数是告诉内核需要监听什么事

struct epoll_event 结构如下:

typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;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 队列里。

2.4、epoll_wait

#include <sys/epoll.h>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 表示函数失败

2.5、epoll 工作原理

当某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关。

struct eventpoll {struct rb_root rbr;              // 红黑树根节点struct rb_root wbr;              // 写事件的红黑树struct list_head rdllist;        // 读等待队列struct list_head wrdllist;       // 写等待队列struct list_head active;         // 活动事件列表struct mutex mtx;                // 互斥锁wait_queue_head_t wait;          // 等待队列int epfd;                        // 文件描述符int maxevents;                   // 最大事件数// 其他字段...
};
  • 每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 logn,其中 n 为树的高度)
  • 而所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到 rdlist 双链表中
  • 在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体

2.6、水平触发 Level Triggered 工作模式(默认模式 LT)

  • 当 epoll 检测到 socket 上事件就绪的时候,可以不立刻进行处理,或者只处理一部分

  • 如上面的例子,由于只读了 1K 数据,缓冲区中还剩 1K 数据,在第二次调用 epoll_wait 时,epoll_wait 仍然会立刻返回并通知 socket 读事件就绪,直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回(也就是只要有事件就绪,没有处理完就回一只返回)。

  • 支持阻塞读写和非阻塞读写


2.7、边缘触发 Edge Triggered 工作模式

如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志,epoll 进入 ET 工作模式

  • 当 epoll 检测到 socket 上事件就绪时,必须立刻处理
  • 如上面的例子,虽然只读了 1K 的数据,缓冲区还剩 1K 的数据,在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了
  • 也就是说,ET 模式下,文件描述符上的事件就绪后,只有一次处理机会(倒逼程序员必须处理完这轮数据)
  • 原因就是如果在ET模式下,上一次的事件未处理完,且之后没有新事件到来,那么epoll_wait将不会返回这些未处理的事件,导致它们一直处于未处理状态,如果有新事件到来,还是会处理掉上一次的事件
  • ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多)
  • Nginx 默认采用 ET 模式使用 epoll
  • 只支持非阻塞的读写

2.8、LT 和 ET 对比

LT 是 epoll 的默认行为

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

2.9、理解 ET 模式和非阻塞文件描述符

使用 ET 模式的 epoll,需要将文件描述设置为非阻塞。

这个不是接口上的要求,而是 “工程实践” 上的要求。

假设这样的场景:

  • 服务器接收到一个 10k 的请求,会向客户端返回一个应答数据。

  • 如果客户端收不到应答,不会发送第二个 10k 请求。

如果服务端写的代码是阻塞式的 read,并且一次只 read 1k 数据的话(read 不能保证一次就把所有的数据都读出来,参考 man 手册的说明,可能被信号打断),剩下的 9k 数据就会待在缓冲区中

此时由于 epoll 是 ET 模式,并不会认为文件描述符读就绪。epoll_wait 就不会再次返回,剩下的 9k 数据会一直在缓冲区中,直到下一次客户端再给服务器写数据,epoll_wait 才能返回

但是问题来了:

  • 服务器只读到 1k 个数据,要 10k 读完才会给客户端返回响应数据
  • 客户端要读到服务器的响应,才会发送下一个请求
  • 客户端发送了下一个请求,epoll_wait 才会返回,才能去读缓冲区中剩余的数据

所以,为了解决上述问题(阻塞 read 不一定能一下把完整的请求读完),于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。

而如果是 LT 没这个问题,只要缓冲区中的数据没读完,就能够让 epoll_wait 返回文件描述符读就绪。


2.10、epoll 使用场景

epoll 的高性能,是有一定的特定场景的。

如果场景选择的不适宜,epoll 的性能可能适得其反。

对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用 epoll。

  • 例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网 APP 的入口服务器,这样的服务器就很适合 epoll
  • 如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll 就并不合适
  • 具体要根据需求和场景特点来决定使用哪种 IO 模型

2.11、epoll 示例

2.11、LT 模式的 epoll 服务器响应程序

相对于前面的 select、poll 服务器响应程序,就修改了对应的 Server.hpp 文件(对应的 Main.cc 也需要改一下<>中的类型)。

LT 模式的 epoll 服务器响应程序

  • EpollServer.hpp文件:主要修改的代码文件
#pragma once#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include <sys/time.h>
#include <string>
#include "Socket.hpp"using namespace socket_ns;class EpollServer
{const static int defaultfd = -1;const static int N = 64;const static int timeout = -1; // 负数阻塞式等待,整数等待的毫秒值public:EpollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()), _epfd(defaultfd){InetAddr client("0", port);_listensock->BuildListenSocket(client);memset(_events, 0, sizeof(_events));_epfd = epoll_create(128);if (_epfd < 0){LOG(FATAL, "epoll create error...");exit(-1);}LOG(DEBUG, "epoll create sucess, epoll fd : %d", _epfd);struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = _listensock->SockFd();// _events[0].events = EPOLLIN;// _events[0].data.fd = _listensock->SockFd();epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);}void AcceptClient(){InetAddr clientaddr;socket_sptr sockefd = _listensock->Accepter(&clientaddr);int fd = sockefd->SockFd();if(fd < 0) return;if (fd >= 0){LOG(DEBUG, "Get new Link ,sockefd is :%d ,client info : %s:%d", fd, clientaddr.Ip().c_str(), clientaddr.Port());}// 把新到的文件描述符交给select托管,使用辅助数组struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = fd;epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);LOG(DEBUG, "%d sockfd add to epoll rbtree", fd);}void ServiceIO(int fd){char buff[1024];ssize_t n = ::recv(fd, buff, sizeof(buff) - 1, 0);if (n > 0){buff[n] = 0;LOG(DEBUG, "client # %s", buff);std::string message = "Server Echo# ";message += buff;::send(fd, message.c_str(), message.size(), 0);}else if (n == 0){LOG(DEBUG, "%d socket closed!", fd);epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr); // 得保证要删除的fd是合法的::close(fd);                                  // 有用户退出,把该文件描述符重置为默认值}else{LOG(DEBUG, "%d recv error!", fd);epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);::close(fd); // 有用户退出,把该文件描述符重置为默认值}}void HandlerRead(int num){for (int i = 0; i < num; i++){uint32_t events = _events[i].events;int sockfd = _events[i].data.fd;if (events & EPOLLIN) // 读事件就绪{if (sockfd == _listensock->SockFd()) // listensock{AcceptClient();}else // 真正的读事件就绪{// socket读事件就绪ServiceIO(sockfd);}}else if (events & EPOLLOUT){// 写事件就绪,后面epoll再做}}}void Loop(){while (true){int n = epoll_wait(_epfd, _events, N, timeout); // 返回请求I/O文件描述符的个数switch (n){case -1:// 错误LOG(FATAL, "epoll wait error ...");break;case 0://  时间到了LOG(DEBUG, "time out ...");break;default:// 处理读文件描述符HandlerRead(n);break;}}}~EpollServer(){::close(_listensock->SockFd());if (_epfd >= 0)::close(_epfd);}private:uint16_t _port;std::unique_ptr<TcpSocket> _listensock;int _epfd;struct epoll_event _events[N];
};

2.12、ET 模式的 epoll 服务器响应程序

后面博客 Reactor 反应堆模式下我们再写。


OKOK,IO 多路转接之 epoll 就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

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

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

相关文章

视频字幕生成:分享6款专业易操作的工具,让创作更简单!

​视频字幕如何添加&#xff1f;日常剪辑Vlog视频时&#xff0c;就需要给视频添加上字幕了。字幕是一个比较重要的元素&#xff0c;它不仅可以帮助听力受损或语言障碍的人士理解内容&#xff0c;还可以让你的视频更加易于理解和吸引观众。 那么如何实现视频字幕生成&#xff0c…

Linux 进程与进程状态

目录 1.进程。 1.进程的概念 2.并行和并发 3.并行和并发的区别&#xff1a; 4.PCB&#xff08;程序控制块&#xff09; 5.进程组与会话。 6.进程状态。 1.进程。 1.进程的概念 进程是操作系统进行资源分配和调度的一个独立单位。每个进程都运行在操作系统的控制之下&…

8.进销存系统(基于springboot的进销存系统)

目录 1.系统的受众说明 2.开发技术与环境配置 2.1 SpringBoot框架 2.2 Java语言简介 2.3 MySQL环境配置 2.4 idea介绍 2.5 mysql数据库介绍 2.6 B/S架构 3.系统分析与设计 3.1 可行性分析 3.1.1 技术可行性 3.1.2 操作可行性 3.1.3经济可行性 3.4.1 数据库…

一些做题中总结的零散的python函数的简单运用

输出保留两位数的小数 将16进制&#xff08;可修改&#xff09;的数进制转换成十进制并输出 大小写转化&#xff0c;第一个是搞成全部大写的&#xff0c;第二个高成全部小写的&#xff0c;最后一个是搞成第一个是大写的其他全部是小写的 将这个n的两边空格去掉 使用print(n,end…

叶国富学得会胖东来吗?

“大家都看不懂就对了&#xff0c;如果都看得懂我就没有机会了。”昨晚&#xff0c;实体零售迎来一则重磅消息&#xff0c;名创优品获得了全国第二大连锁超市永辉超市的大股东身份。在资本市场负反馈的压力下&#xff0c;名创优品创始人叶国富有了上述回应。 消息公布后&#x…

2.以太网

局域网 局域网: Local Area Networks (LAN) 网络大小分类 局域网园区网(可以理解为企业网)城域网 广域网是一个网络连接的技术&#xff0c;并非多大范围的网络 网关 为局域网内的用户提供了一扇门&#xff0c;通过网关可以访问到别的网络。这个门&#xff0c;就叫网关 以…

解决你的IDE在使用的时候测试单元@Test在创建Scanner对象是键盘键入不了的问题;

插播一条快讯&#xff0c;我在我的ide中新创建 了project后发现我的测试单元不好使了&#xff0c;即 import org.junit.Test; 这个包在创建Scanner对象接受键盘时&#xff0c;控制台输入时没有任何反应&#xff0c;键入不了了&#xff0c;我的问题出现原因可能是我导入了JDBC…

BitSet-解决数据压缩问题

一、问题引入 假设QQ音乐服务器上有9000万首音乐&#xff0c;用户按照歌名来搜索歌曲&#xff0c;如何使得满足这一需求所需的数据占用的内存空间最小以及用户搜索歌曲速度更快 二、分析问题 1、为了满足使得数据占用的内存更小&#xff0c;可以采用映射的思路&#xff0c;按…

【2024W35】肖恩技术周刊(第 13 期):肉,好次!

周刊内容: 对一周内阅读的资讯或技术内容精品&#xff08;个人向&#xff09;进行总结&#xff0c;分类大致包含“业界资讯”、“技术博客”、“开源项目”和“工具分享”等。为减少阅读负担提高记忆留存率&#xff0c;每类下内容数一般不超过3条。 更新时间: 星期天 历史收录:…

【C++算法】链表

知识总结 常用技术&#xff1a; 1.画图&#xff01;&#xff01;——>直观形象便于理解 2.引入虚拟”头结点“ 便于处理边界情况方便对链表操作 3.不要吝啬空间&#xff0c;大胆定义变量 4.快慢双指针——判环、找链表中环的入口、找链表中倒数第n个节点 链表中的常用…

移动数组中数字的方法(c语言)

1.移动一维数组中的内容;若数组中有n个整数&#xff0c;要求把下标从0到p(含p&#xff0c;p小于等于n-1&#xff09;的数组元素平移到数组的最后。 例如&#xff0c;一维数组中的原始内容为:1,2,3,4,5,6,7,8,9,10;p的值为3。 移动后&#xff0c;一维数组中的内容应为:5,6,7,8…

融会贯通记单词,绝对丝滑,一天轻松记几百

如果我将flower(花&#xff09;、flat(公寓)、floor(地板)、plane(飞机)几个单词放在一起&#xff0c;你会怎么来记忆这样的一些单词呢&#xff1f; 我们会发现&#xff0c;我们首先可以将plane去掉&#xff0c;因为它看上去几乎就是一个异类。这样&#xff0c;我们首先就可以将…

力扣958:判断二叉树是否为完全二叉树

给你一棵二叉树的根节点 root &#xff0c;请你判断这棵树是否是一棵 完全二叉树 。 在一棵 完全二叉树 中&#xff0c;除了最后一层外&#xff0c;所有层都被完全填满&#xff0c;并且最后一层中的所有节点都尽可能靠左。最后一层&#xff08;第 h 层&#xff09;中可以包含 …

Pyinstaller打包python程序为exe时 程序多线程导致打开非常多窗口解决

装了个Pyinstaller打包exe pip install Pyinstaller 打包命令 Pyinstaller -F main.py Pyinstaller -F -w main.py #不带控制台 Pyinstaller -F -w -i 1.ico main.py #指定图标不带控制台 打包完的exe一运行开了一坨窗口&#xff0c;一眼多线程&#xff0c;我程序里的多线程如…

内容生态短缺,Rokid AR眼镜面临市场淘汰赛

AR是未来&#xff0c;但在技术路径难突破、生态系统难建设&#xff0c;且巨头纷纷下场的背景下&#xff0c;Rokid能坚持到黎明吗&#xff1f; 转载&#xff1a;科技新知 原创 作者丨王思原 编辑丨蕨影 苹果Vision Pro的成功量产和发售&#xff0c;以及热门游戏《黑神话》等在A…

Mac电脑可以只装Windows系统吗 苹果电脑也可以清除垃圾吗

选Mac还是Windows&#xff0c;一直是个有争议的话题。习惯Windows操作模式的用户&#xff0c;甚至想在Mac电脑上安装Windows操作系统。其实&#xff0c;只要掌握Mac系统的清理技巧&#xff0c;苹果电脑也能带来良好的使用体验。有关Mac电脑可以只装Windows系统吗&#xff0c;苹…

将Pytorch环境打包,快速部署到另一台机器上(在没有网络,或者网络环境不好的情况下推荐使用)

打包PyTorch环境 当您需要在不同的机器上快速部署包含PyTorch的Python环境时&#xff0c;使用conda-pack是一个很好的选择。conda-pack可以打包一个完整的Conda环境&#xff0c;包括所有已安装的包和依赖项&#xff0c;使其能够轻松地在其他机器上还原。 步骤一&#xff1a;…

My_string 运算符重载,My_stack

思维导图 将My_string类中的所有能重载的运算符全部进行重载 、[] 、>、<、、>、<、! 、&#xff08;可以加等一个字符串&#xff0c;也可以加等一个字符&#xff09;、输入输出(<< 、 >>) My_string my_string.h #ifndef MY_STRING_H #define MY_…

【Web】初识Web和Tomcat服务器

目录 前言 一、认识web 1. 软件架构模式 2. web资源 3. URL请求路径&#xff08;统一资源定位符&#xff09; 二、Tomcat服务器 1. 简介 2. tomcat服务器的目录结构 3.使用tomcat服务器启动失败的常见原因 3.1 端口冲突 3.2 jdk环境变量配置出错 三、使用Tomcat发布…