当前位置: 首页 > news >正文

【C++高并发服务器WebServer】-15:poll、epoll详解及实现

在这里插入图片描述

本文目录

  • 一、poll
  • 二、epoll
    • 2.1 相对poll和select的优点
    • 2.2 epoll的api
    • 2.3 epoll的demo实现
    • 2.5 epoll的工作模式

一、poll

poll是对select的一个改进,我们先来看看select的缺点。

在这里插入图片描述
我们来看看poll的实现。

struct pollfd {int fd; /* 委托内核检测的文件描述符 */short events; /* 委托内核检测文件描述符的什么事件 */short revents; /* 文件描述符实际发生的事件 */
};struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明如下。
fds:是struct pollfd结构体数组,这是一个需要检测的文件描述符集合。

当内核检测到有变动之后,有需要修改的,会直接修改revents,不需要修改events了,相对select来说,就不需要每次重置fds集合。

除此之外,相对于select来说,并没有1024的限制。
nfds是第一个参数数组中最后一个有效元素的下标+1。

timeout,注意这个是int类型的,当为0时代表不阻塞,当为-1时表示阻塞,当检测到需要检测的文件描述符发生了变化,解除阻塞。>0表示阻塞的时长。

poll函数返回值为-1时表示失败,>0会返回n,表示检测到集合中有n个描述符发生了变化。

在这里插入图片描述

poll的服务端实现代码如下。

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 监听listen(lfd, 8);// 初始化检测的文件描述符数组struct pollfd fds[1024];for(int i = 0; i < 1024; i++) {fds[i].fd = -1;fds[i].events = POLLIN;}fds[0].fd = lfd;int nfds = 0;while(1) {// 调用poll系统函数,让内核帮检测哪些文件描述符有数据int ret = poll(fds, nfds + 1, -1);if(ret == -1) {perror("poll");exit(-1);} else if(ret == 0) {continue;} else if(ret > 0) {// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变if(fds[0].revents & POLLIN) {// 表示有新的客户端连接进来了struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);// 将新的文件描述符加入到集合中for(int i = 1; i < 1024; i++) {if(fds[i].fd == -1) {fds[i].fd = cfd;fds[i].events = POLLIN;break;}}// 更新最大的文件描述符的索引nfds = nfds > cfd ? nfds : cfd;}for(int i = 1; i <= nfds; i++) {if(fds[i].revents & POLLIN) {// 说明这个文件描述符对应的客户端发来了数据char buf[1024] = {0};int len = read(fds[i].fd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");close(fds[i].fd);fds[i].fd = -1;} else if(len > 0) {printf("read buf = %s\n", buf);write(fds[i].fd, buf, strlen(buf) + 1);}}}}}close(lfd);return 0;
}

对应的客户端代码我们继续沿用之前的即可。

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}int num = 0;while(1) {char sendBuf[1024] = {0};sprintf(sendBuf, "send data %d", num++);write(fd, sendBuf, strlen(sendBuf) + 1);// 接收int len = read(fd, sendBuf, sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", sendBuf);} else {printf("服务器已经断开连接...\n");break;}// sleep(1);usleep(1000);}close(fd);return 0;
}

二、epoll

首先调用epoll_create实现一个epoll的实例,这个epoll实例是在内核区,是结构体类型,可以理解成一块数据。返回值是一个文件描述符,那我们就可以通过这个文件描述符来操作这块内核当中的epoll数据(通过epoll提供的一些api来进行操作)。

eventpoll中有两个最关键的数据,就是rbrrdlist,也就是红黑树双向就绪链表

rbr记录需要检测的文件描述符。(之前需要把一些表从用户态拷贝到内核态,现在是直接在内核态,效率高了很多。另外现在是红黑树,之前是链表,红黑树的遍历效率也高很多。)

rdlist是检测文件描述符当中哪些是有数据发生改变的。

在函数epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev)中最后一个参数evstruct epoll_event类型,需要设置eventsev.data.fd

调用epoll_wait之后内核就会去检测rbr里面的文件描述符是否有对应的数据改变。如果有改变的(就绪的),就会把文件描述符的信息放到rdlist中,然后会把这个list拷贝到用户区,这样用户区直接遍历这几个fd,就可以进行对应的读写操作了。

在这里插入图片描述

2.1 相对poll和select的优点

时间复杂度方面,每次调用 select 或 poll 时,内核需要遍历所有被监控的文件描述符,检查它们的状态。select 和 poll 的时间复杂度是 O(n)。当文件描述符数量很大时(例如成千上万个),这种线性扫描的效率会非常低。

epoll 的时间复杂度是 O(1),epoll 使用红黑树和双向链表来管理文件描述符。当文件描述符的状态发生变化时,内核会将其加入到就绪链表中,用户程序只需要检查就绪链表即可,而不需要遍历所有文件描述符。

文件描述符数量限制方面,默认情况下,select 只能监控最多 1024 个文件描述符(由 FD_SETSIZE`定义)。如果需要监控更多的文件描述符,需要修改内核参数并重新编译程序。poll 使用数组来存储文件描述符,理论上可以监控任意数量的文件描述符。但当文件描述符数量很大时,遍历整个数组的效率会非常低。

epoll 可以轻松支持数万个甚至更多的文件描述符。它使用红黑树来存储文件描述符,查找和插入的效率很高。

用户态和内核态的数据拷贝方面,每次调用 select 或 poll 时,都需要将文件描述符集合从用户态拷贝到内核态:当文件描述符数量很大时,这种拷贝操作会带来较大的开销。

对于epoll,文件描述符只需要通过 epoll_ctl 添加到内核事件表中一次,后续不需要重复拷贝。当文件描述符状态变化时,内核会直接将事件放入就绪链表中,用户程序通过 epoll_wait 获取就绪事件。

事件触发模式方面,select 和 poll 只支持水平触发(Level-Triggered,LT)模式:如果文件描述符的状态满足条件(例如有数据可读),select 和 poll 会一直通知用户程序,直到状态发生变化。

在这里插入图片描述

epoll 支持水平触发(LT)和边缘触发(Edge-Triggered,ET)模式:
水平触发(LT):与 select 和 poll 的行为相同,只要文件描述符的状态满足条件,就会一直通知用户程序。边缘触发(ET):只有当文件描述符的状态发生变化时,才会通知用户程序。这种模式可以减少重复通知的次数,提高效率。但是需要用户程序一次性处理完所有数据,否则可能会丢失数据。

内核实现机制方面,select 和 poll 是基于轮询的机制:每次调用时,内核需要遍历所有文件描述符,检查它们的状态。这种机制在大规模并发场景下效率较低。

epoll 是基于事件回调的机制:内核会为每个文件描述符注册回调函数,当文件描述符的状态发生变化时,内核会调用回调函数将其加入到就绪链表中。这种机制避免了不必要的遍历,效率更高。

2.2 epoll的api

头文件如下。

#include <sys/epoll.h>

/创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。【从linux内核2.6.8开始,size这个参数已经被忽略了,但是必须大于0。】

int epoll_create(int size);
- 参数:size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:-1 : 失败> 0 : 文件描述符,操作epoll实例的

对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:- epfd : epoll实例对应的文件描述符- op : 要进行什么操作EPOLL_CTL_ADD: 添加EPOLL_CTL_MOD: 修改(比如从读事件改成写事件)EPOLL_CTL_DEL: 删除- fd : 要检测的文件描述符- event : 检测文件描述符什么事情

epoll_event是检测事件的结构体,定义如下。

struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:- EPOLLIN - EPOLLOUT - EPOLLERR- EPOLLET (设置边沿触发)

在其中,又有一个联合体epoll_data_t,定义如下。通过联合体,用户可以选择存储不同类型的数据,如指针、文件描述符、32 位或 64 位整数。

ptr是一个指向任意类型的指针。用户可以将与事件相关的任意数据存储在这个指针中,例如指向某个结构体的指针。这种方式非常灵活,可以存储用户自定义的数据结构。
fd,这是 epoll 最常用的用途之一,直接存储与事件相关的文件描述符。
u32是一个 32 位的无符号整数。用户可以存储一些简单的整数值作为用户数据。u64同理。

联合体 epoll_data_t 的设计允许用户根据需要选择存储不同类型的数据。联合体的特性是所有成员共享同一块内存,因此在任何时刻,联合体中只有一个成员是有效的。用户可以根据实际需求选择存储哪种类型的数据。

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, inttimeout);- 参数:- epfd : epoll实例对应的文件描述符- events : 传出参数,保存了发送了变化的文件描述符的信息- maxevents : 第二个参数结构体数组的大小- timeout : 阻塞时间- 0 : 不阻塞- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
-		 > 0 : 阻塞的时长(毫秒)- 返回值:- 成功,返回发送变化的文件描述符的个数 > 0- 失败 -1

这里有个问题是,在使用 epoll 时,epoll_ctl 函数确实已经将文件描述符(fd)注册到了 epoll 实例中,但 epoll_event 结构体中的 data.fd 仍然需要存储文件描述符的原因主要有以下几点:

首先epoll_ctl是用于将文件描述符注册到 epoll 实例中,并设置相关的事件类型(如 EPOLLIN、EPOLLOUT 等)。它的作用是告诉 epoll 哪些文件描述符需要被监控,以及监控哪些类型的事件。

epoll_event 中 用于在 epoll_wait 调用时返回检测到的事件。
它的作用是告诉用户哪些文件描述符发生了事件,以及发生了哪些类型的事件。epoll_event 中的 data.fd 是为了方便用户在 epoll_wait 返回后,能够直接获取到发生事件的文件描述符。

epoll_wait 返回时,它会返回一个 epoll_event 数组,每个 epoll_event 表示一个发生事件的文件描述符及其事件类型。通过在 epoll_event 中存储 fd,用户可以直接从 epoll_event 中获取到发生事件的文件描述符,而无需额外查找。

2.3 epoll的demo实现

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 监听listen(lfd, 8);// 调用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);struct epoll_event epevs[1024];while(1) {int ret = epoll_wait(epfd, epevs, 1024, -1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n", ret);for(int i = 0; i < ret; i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据达到,有客户端连接struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);epev.events = EPOLLIN | EPOLLOUT; //监听的事件比较多,所以每一种事件在下方都需要进行对应的处理。epev.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);} else {if(epevs[i].events & EPOLLOUT) {continue;}   // 有数据到达,需要通信char buf[1024] = {0};int len = read(curfd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);} else if(len > 0) {printf("read buf = %s\n", buf);write(curfd, buf, strlen(buf) + 1);}}}}close(lfd);close(epfd);return 0;
}

2.5 epoll的工作模式

epoll 有两种工作模式:LT(水平触发)模式和 ET(边沿触发)模式。在 LT 模式中,当内核检测到文件描述符(fd)的读缓冲区中有数据时,会通知用户。如果用户没有读取数据,数据会一直保留在缓冲区中,epoll 会持续通知用户。即使用户只读取了一部分数据,epoll 也会继续通知,直到缓冲区的数据被完全读走。LT 模式同时支持阻塞(block)和非阻塞(non-block)的 socket,它是一种缺省的工作方式,内核会持续告知用户文件描述符是否就绪,并允许用户对这个就绪的 fd 进行 I/O 操作。如果用户不进行任何操作,内核会继续发送通知。

相比之下,ET 模式是一种高速工作方式,仅支持非阻塞 socket。在这种模式下,内核仅在文件描述符从未就绪变为就绪时通过 epoll 通知用户一次。一旦通知,内核会假定用户知道文件描述符已经就绪,并且不会再为该文件描述符发送更多的就绪通知,除非用户执行了某些操作导致文件描述符不再处于就绪状态。在 ET 模式中,如果用户不对 fd 执行 I/O 操作,从而使得它再次变为未就绪状态,内核不会再次发送通知。这种模式显著减少了 epoll 事件被重复触发的次数,因此比 LT 模式更高效。在 ET 模式下工作时,必须使用非阻塞套接字,以避免由于单个文件句柄的阻塞读/写操作导致处理多个文件描述符的任务饿死。

需要特别注意的是,ET模式中,如果用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不会再通知了。

如果使用了ET模式,那么在监听到有客户端连接之后,对cfd的属性需要设置非阻塞。

相关文章:

【C++高并发服务器WebServer】-15:poll、epoll详解及实现

本文目录 一、poll二、epoll2.1 相对poll和select的优点2.2 epoll的api2.3 epoll的demo实现2.5 epoll的工作模式 一、poll poll是对select的一个改进&#xff0c;我们先来看看select的缺点。 我们来看看poll的实现。 struct pollfd {int fd; /* 委托内核检测的文件描述符 */s…...

Visual Studio 2022 中使用 Google Test

要在 Visual Studio 2022 中使用 Google Test (gtest)&#xff0c;可以按照以下步骤进行&#xff1a; 安装 Google Test&#xff1a;确保你已经安装了 Google Test。如果没有安装&#xff0c;可以通过 Visual Studio Installer 安装。在安装程序中&#xff0c;找到并选择 Googl…...

Office/WPS接入DeepSeek等多个AI工具,开启办公新模式!

在现代职场中&#xff0c;Office办公套件已成为工作和学习的必备工具&#xff0c;其功能强大但复杂&#xff0c;熟练掌握需要系统的学习。为了简化操作&#xff0c;使每个人都能轻松使用各种功能&#xff0c;市场上涌现出各类办公插件。这些插件不仅提升了用户体验&#xff0c;…...

Meta AI 最近推出了一款全新的机器学习框架ParetoQ,专门用于大型语言模型的4-bit 以下量化

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

操作系统—进程与线程

补充知识 PSW程序状态字寄存器PC程序计数器&#xff1a;存放下一条指令的地址IR指令寄存器&#xff1a;存放当前正在执行的指令通用寄存器&#xff1a;存放其他一些必要信息 进程 进程&#xff1a;进程是进程实体的运行过程&#xff0c;是系统进行资源分配和调度的一个独立单位…...

团队:前端开发工期参考 / 防止工期不足、过足、工期打架

一、前端开发工期参考 序号功能 / 模块 / 页面 / 描述pc端&#xff08;数值为比例&#xff09;小程序端&#xff08;数值为比例&#xff09;1简单页面 / 常规页面1&#xff1a;12复杂页面&#xff08;功能复杂 / 逻辑复杂&#xff09;1&#xff1a;1.5 / 1&#xff1a;2 / …...

APL语言的云计算

APL语言的云计算&#xff1a;一种灵活而高效的编程方式 引言 随着信息技术的迅猛发展&#xff0c;云计算已经成为现代计算的重要组成部分。云计算不仅带来了计算资源的高效利用&#xff0c;也引发了新一轮的技术革命。在这个背景下&#xff0c;APL&#xff08;A Programming …...

idea启动报错# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffccf76e433

# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc0x00007ffccf76e433, pid17288, tid6696 # # JRE version: (11.0.248) (build ) # Java VM: OpenJDK 64-Bit Server VM (11.0.248-LTS, mixed mode, sharing, tiered, compressed oops, g1 gc, windows-amd64) 不知道为什么…...

C++拷贝构造函数与运算符重载应该注意的一个问题?

看下面的例子&#xff1a; class TestClass { public:char* _pdata;size_t _nLength;public:TestClass(const TestClass& other) {_nLength other._nLength;_pdata new char[_nLength];memcpy((void*)_pdata,other._pdata, _nLength 1);}TestClass(const char* pstr) {…...

[7] 游戏机项目说明

[7] 游戏机项目说明 在这节课中&#xff0c;我们将学习如何基于FreeRTOS开发一个简单的游戏项目。我们会使用一个开源项目nwatch&#xff0c;它是一个基于STM32的开源手表&#xff0c;包含了三个游戏。我们的目标是将这个游戏移植到我们的开发板上&#xff0c;并逐步使用FreeR…...

“深入浅出”系列之C++:(20)C++17

C17的新拓展 并行算法&#xff1a; C17引入了并行STL算法&#xff0c;允许使用多个线程并行处理元素&#xff0c;提高了在多核系统上的性能。 示例代码&#xff1a;std::sort(std::execution::par, v.begin(), v.end()); 类模板参数推导&#xff08;CTAD&#xff09;&#…...

.net一些知识点5

1.dot Net带out的参数如何使用 string name;//假设这个参数带out TestMethod(1,out name);//一定要有out 方法体中&#xff0c;一定要有out参数的赋值&#xff0c;并且能输出 2.参数的传递方式有哪些 a.值传递 b.引用传递 ref c.输出传递 out 3.设计模式知道哪些 3.us…...

(七)QT——消息事件机制&绘图&文件

目录 前言 消息事件机制 (Event System) 绘图 (Graphics & Drawing) 绘图设备 Qt 提供的主要绘图设备 Qt 主要绘图设备的特点 各个绘图设备的详细介绍 文件处理 (File Handling) 总结 前言 QT 是一个非常强大的图形用户界面&#xff08;GUI&#xff09;开发框架&…...

【虚幻引擎UE】AOI算法介绍与实现案例

【虚幻引擎UE】AOI算法介绍与实现 一、AOI算法介绍AOI算法的典型应用场景二、AOI相关算法1. 边界框法(Bounding Box Method)2. 动态AOI算法3. 布尔运算(Boolean Operations)4. 四叉树(Quadtree)5. R树(R-Tree)6. 圆形AOI算法7. 网格分割(Grid Partitioning)8. 多边形…...

python学opencv|读取图像(六十)先后使用cv2.erode()函数和cv2.dilate()函数实现图像处理

【1】引言 前序学习进程中&#xff0c;先后了解了使用cv2.erode()函数和cv2.dilate()函数实现图像腐蚀和膨胀处理的效果&#xff0c;相关文章链接为&#xff1a; python学opencv|读取图像&#xff08;五十八&#xff09;使用cv2.erode()函数实现图像腐蚀处理-CSDN博客 pytho…...

AI能帮谷歌SEO做什么?

现在没用过AI写内容的人&#xff0c;应该不多了&#xff0c;用ChatGPT写文章&#xff0c;用MidJourney画图&#xff0c;用各种工具做调研&#xff0c;AI已经成为SEO玩家的“标配”。但AI到底能帮SEO做到什么&#xff1f;省钱&#xff1f;省时间&#xff1f;还是更重要的东西&am…...

SSA-TCN麻雀算法优化时间卷积神经网络时间序列预测未来Matlab实现

SSA-TCN麻雀算法优化时间卷积神经网络时间序列预测未来Matlab实现 目录 SSA-TCN麻雀算法优化时间卷积神经网络时间序列预测未来Matlab实现预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现SSA-TCN麻雀算法优化时间卷积神经网络时间序列预测未来&#xff08;优…...

【机器学习】数据预处理之数据归一化

数据预处理之数据归一化 一、摘要二、数据归一化概念三、数据归一化实现方法3.1 最值归一化方法3.2 均值方差归一化方法 一、摘要 本文主要讲述了数据归一化&#xff08;Feature Scaling&#xff09;的重要性及其方法。首先通过肿瘤大小和发现时间的例子&#xff0c;说明了不同…...

【专题】2024-2025人工智能代理深度剖析:GenAI 前沿、LangChain 现状及演进影响与发展趋势报告汇总PDF洞察(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p39630 在科技飞速发展的当下&#xff0c;人工智能代理正经历着深刻的变革&#xff0c;其能力演变已然成为重塑各行业格局的关键力量。从早期简单的规则执行&#xff0c;到如今复杂的自主决策与多智能体协作&#xff0c;人工智能代理…...

非递减子序列(力扣491)

这道题的难点依旧是去重&#xff0c;但是与之前做过的子集类问题的区别就是&#xff0c;这里是求子序列&#xff0c;意味着我们不能先给数组中的元素排序。因为子序列中的元素的相对位置跟原数组中的相对位置是一样的&#xff0c;如果我们改变数组中元素的顺序&#xff0c;子序…...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

镜像里切换为普通用户

如果你登录远程虚拟机默认就是 root 用户&#xff0c;但你不希望用 root 权限运行 ns-3&#xff08;这是对的&#xff0c;ns3 工具会拒绝 root&#xff09;&#xff0c;你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案&#xff1a;创建非 roo…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

CSS | transition 和 transform的用处和区别

省流总结&#xff1a; transform用于变换/变形&#xff0c;transition是动画控制器 transform 用来对元素进行变形&#xff0c;常见的操作如下&#xff0c;它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storms…...

MySQL:分区的基本使用

目录 一、什么是分区二、有什么作用三、分类四、创建分区五、删除分区 一、什么是分区 MySQL 分区&#xff08;Partitioning&#xff09;是一种将单张表的数据逻辑上拆分成多个物理部分的技术。这些物理部分&#xff08;分区&#xff09;可以独立存储、管理和优化&#xff0c;…...

面试高频问题

文章目录 &#x1f680; 消息队列核心技术揭秘&#xff1a;从入门到秒杀面试官1️⃣ Kafka为何能"吞云吐雾"&#xff1f;性能背后的秘密1.1 顺序写入与零拷贝&#xff1a;性能的双引擎1.2 分区并行&#xff1a;数据的"八车道高速公路"1.3 页缓存与批量处理…...

13.10 LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析

LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析 LanguageMentor 对话式训练系统架构与实现 关键词:多轮对话系统设计、场景化提示工程、情感识别优化、LangGraph 状态管理、Ollama 私有化部署 1. 对话训练系统技术架构 采用四层架构实现高扩展性的对话训练…...