细谈 Linux 中的多路复用epoll
大家好,我是 V 哥。在 Linux 中,epoll
是一种多路复用机制,用于高效地处理大量文件描述符(file descriptor, FD)事件。与传统的select
和poll
相比,epoll
具有更高的性能和可扩展性,特别是在大规模并发场景下,比如高并发服务器。
以下是epoll
的核心数据结构和实现原理:
1. epoll
的核心数据结构
在 Linux 内核中,epoll
的实现涉及多个核心数据结构,主要包括以下几个:
(1) epoll
实例
epoll
在创建时,会生成一个与之关联的实例,这个实例在内核中是一个epoll
文件对象(struct file
),并且与用户态的epoll
文件描述符(FD)对应。该实例负责维护和管理所有加入的事件。
(2) 事件等待队列(epitem
)
epoll
中的每个事件都被封装成一个epitem
结构。该结构体主要包括以下几个关键内容:
- 指向被监听文件的指针:用于标识监听的文件对象。
- 事件类型和事件掩码:指定关注的事件类型(如可读、可写、异常等)。
- 双向链表节点:用于将所有的
epitem
结构体组织成链表(或红黑树)。
(3) 红黑树(RB-Tree)
为了快速查找和管理epitem
,epoll
使用红黑树将所有的epitem
组织起来。每个被监听的文件描述符及其事件类型会存储在红黑树中,通过这种方式,可以在事件添加、删除、修改时实现高效的查找和管理。
(4) 就绪队列(Ready List)
当监听的文件描述符上发生指定的事件时,epoll
会将该文件描述符的事件加入一个就绪队列。这个队列是一个双向链表,存储所有准备好处理的epitem
。当用户调用epoll_wait
时,内核从该队列中取出满足条件的事件并返回。
2. epoll
的三种操作
epoll
提供三种主要的操作接口:epoll_create
、epoll_ctl
和 epoll_wait
。
(1) epoll_create
epoll_create
用于创建一个epoll
实例,并返回一个文件描述符。它会在内核中分配epoll
数据结构,并初始化就绪队列、红黑树等结构。它主要完成以下任务:
- 分配一个
epoll
实例,并初始化相关的数据结构。 - 创建一个文件描述符供用户引用。
(2) epoll_ctl
epoll_ctl
用于将事件添加到epoll
实例中,或从epoll
实例中移除,或修改现有事件。具体操作包括:
- 添加事件(EPOLL_CTL_ADD):将新事件添加到
epoll
中,即将文件描述符及其事件掩码包装成epitem
结构体,然后插入红黑树。 - 删除事件(EPOLL_CTL_DEL):将事件从
epoll
实例中移除,即从红黑树中删除对应的epitem
。 - 修改事件(EPOLL_CTL_MOD):修改现有的事件,比如修改事件掩码或回调方式。
通过红黑树结构,epoll_ctl
操作的添加、删除、修改事件在平均时间复杂度上为 (O(\log N)),相较于poll
的线性复杂度更具性能优势。
(3) epoll_wait
epoll_wait
用于等待文件描述符上的事件,直到有事件触发或超时。其主要过程包括:
- 遍历就绪队列,将所有已经准备好的事件放入用户态缓冲区,并清空队列。
- 如果没有事件发生,内核会让调用线程进入休眠状态,并在监听的事件发生后唤醒。
epoll
会利用中断机制高效地唤醒阻塞在epoll_wait
上的线程,从而实现事件驱动的处理方式。
epoll_wait
只需遍历就绪队列中的事件,而不是遍历所有的监听事件,这使得性能相较于select
和poll
有显著提升。特别是在大量文件描述符中仅有少数活跃时,epoll_wait
的优势更为明显。
3. epoll
的触发模式
epoll
提供两种触发模式来控制事件的触发方式:
(1) 水平触发(LT, Level Triggered)
在默认的水平触发模式下,只要文件描述符上有指定的事件(如数据可读),每次调用epoll_wait
都会返回此事件,除非事件被处理(如数据被读走)。这是与poll
和select
一致的行为。
(2) 边缘触发(ET, Edge Triggered)
在边缘触发模式下,epoll_wait
只会在事件第一次发生时通知,之后即使该事件条件一直满足(如数据仍可读),也不会再次触发,除非事件条件有新的变化。该模式能够减少不必要的系统调用次数,但要求应用程序在接收到通知后必须一次性处理所有数据,否则可能会错过事件。
4. epoll
的优缺点
优点:
- 高效的事件监听:使用红黑树管理监听事件,提高了事件的增删查效率。
- 事件驱动的高并发处理:通过边缘触发模式,减少系统调用次数,适合高并发场景。
- 就绪事件分离:就绪队列与监听列表分离,不必遍历所有文件描述符,从而大大提升了性能。
缺点:
- 只支持 Linux:
epoll
是 Linux 特有的实现,跨平台兼容性较差。 - 编程复杂度:相比
select
和poll
,epoll
需要更精细的控制,特别是在边缘触发模式下应用程序需要处理全部数据,以防止事件丢失。
5. Java NIO 如何使用多路复用
下面 V 哥用案例来详细说一说Java 中的多路复用。在 Java NIO 中,Selector
类实现了多路复用机制,底层使用 epoll
或 poll
实现。Java NIO 中的多路复用非常适合处理大量并发连接,比如在高并发的服务器场景中。以下是使用 Java NIO 和 Selector
创建一个简化的聊天服务器示例,通过多路复用处理多个客户端连接。
示例:NIO 实现的聊天服务器
这个服务器使用 ServerSocketChannel
来监听客户端连接,通过 Selector
监听和管理事件,并使用 SocketChannel
处理每个连接。客户端连接后可以发送消息,服务器会将消息广播给所有其他连接的客户端。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;public class WGNioChatServer {private final int port;private Selector selector;private ServerSocketChannel serverSocketChannel;private final Map<SocketChannel, String> clientNames = new HashMap<>(); // 保存客户端名称public WGNioChatServer(int port) {this.port = port;}public void start() throws IOException {// 初始化服务器通道和选择器selector = Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(port));serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("Chat server started on port " + port);while (true) {// 轮询准备就绪的事件selector.select();Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {handleAccept();} else if (key.isReadable()) {handleRead(key);}}}}// 处理新客户端连接private void handleAccept() throws IOException {SocketChannel clientChannel = serverSocketChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);String clientAddress = clientChannel.getRemoteAddress().toString();clientNames.put(clientChannel, clientAddress);System.out.println("Connected: " + clientAddress);broadcast("User " + clientAddress + " joined the chat", clientChannel);}// 读取客户端消息并广播给其他客户端private void handleRead(SelectionKey key) throws IOException {SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(256);int bytesRead = clientChannel.read(buffer);if (bytesRead == -1) {// 客户端断开连接String clientName = clientNames.get(clientChannel);System.out.println("Disconnected: " + clientName);clientNames.remove(clientChannel);key.cancel();clientChannel.close();broadcast("User " + clientName + " left the chat", clientChannel);return;}buffer.flip();String message = new String(buffer.array(), 0, bytesRead);System.out.println(clientNames.get(clientChannel) + ": " + message.trim());broadcast(clientNames.get(clientChannel) + ": " + message, clientChannel);}// 向所有客户端广播消息private void broadcast(String message, SocketChannel sender) throws IOException {ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());for (SelectionKey key : selector.keys()) {Channel targetChannel = key.channel();if (targetChannel instanceof SocketChannel && targetChannel != sender) {SocketChannel clientChannel = (SocketChannel) targetChannel;clientChannel.write(buffer.duplicate());}}}public static void main(String[] args) throws IOException {int port = 123456;new WGNioChatServer(port).start();}
}
代码说明
-
初始化服务器:
- 使用
ServerSocketChannel.open()
创建服务器套接字通道,配置为非阻塞模式,并绑定端口。 - 使用
Selector.open()
创建选择器并将ServerSocketChannel
注册到Selector
上,监听连接事件SelectionKey.OP_ACCEPT
。
- 使用
-
事件处理:
selector.select()
会阻塞直到至少一个通道变为就绪状态。key.isAcceptable()
:处理新的客户端连接,将新客户端通道注册到选择器中,监听读取事件SelectionKey.OP_READ
。key.isReadable()
:读取来自客户端的消息并广播给所有其他客户端。
-
广播机制:
- 使用
Selector.keys()
遍历所有注册的通道(包含当前连接的所有客户端),将消息写入除发送者之外的所有客户端通道。
- 使用
业务场景扩展
在实际业务中,可以进一步优化或扩展这个代码,比如:
- 增加心跳检测来处理空闲客户端连接,避免资源浪费。
- 将每个
SocketChannel
放到单独的线程池中处理,以实现更精细的并发控制。 - 实现消息格式协议(如 JSON 或 Protobuf)来传输结构化数据。
6. 优化一下
在实际业务场景中,我们可以基于 Java NIO 对该聊天服务器进行如下优化:
- 心跳检测:定期检测客户端连接是否空闲,断开长时间无响应的连接,以节省资源。
- 线程池处理:将每个
SocketChannel
的消息处理放入线程池,以避免阻塞主线程,提高并发性能。 - 消息协议格式:使用 JSON 格式封装消息内容,使客户端与服务端之间的消息更加结构化。
下面是优化后的代码实现:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.*;
import com.fasterxml.jackson.databind.ObjectMapper;public class EnhancedNioChatServer {private final int port;private Selector selector; // 多路复用器,负责管理多个通道private ServerSocketChannel serverSocketChannel; // 服务器通道,用于监听客户端连接private final Map<SocketChannel, String> clientNames = new HashMap<>(); // 存储客户端名称private final Map<SocketChannel, Long> lastActiveTime = new ConcurrentHashMap<>(); // 存储客户端最后活动时间private final ScheduledExecutorService heartbeatScheduler = Executors.newScheduledThreadPool(1); // 心跳检测定时任务private final ExecutorService workerPool = Executors.newFixedThreadPool(10); // 处理客户端请求的线程池private final ObjectMapper objectMapper = new ObjectMapper(); // 用于 JSON 序列化的对象public EnhancedNioChatServer(int port) {this.port = port;}public void start() throws IOException {// 初始化服务器通道和选择器selector = Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false); // 配置非阻塞模式serverSocketChannel.bind(new InetSocketAddress(port)); // 绑定端口serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册连接接收事件System.out.println("Chat server started on port " + port);// 启动心跳检测任务startHeartbeatCheck();while (true) {selector.select(); // 阻塞直到至少有一个事件发生Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove(); // 防止重复处理if (key.isAcceptable()) {handleAccept(); // 处理客户端连接} else if (key.isReadable()) {handleRead(key); // 处理客户端的消息读取}}}}// 处理新的客户端连接private void handleAccept() throws IOException {SocketChannel clientChannel = serverSocketChannel.accept(); // 接受新的客户端连接clientChannel.configureBlocking(false); // 设置非阻塞模式clientChannel.register(selector, SelectionKey.OP_READ); // 注册读事件String clientAddress = clientChannel.getRemoteAddress().toString();clientNames.put(clientChannel, clientAddress); // 保存客户端地址lastActiveTime.put(clientChannel, System.currentTimeMillis()); // 记录最后活动时间System.out.println("Connected: " + clientAddress);broadcast(new Message("System", "User " + clientAddress + " joined the chat"), clientChannel);}// 处理读取客户端消息private void handleRead(SelectionKey key) {SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(256); // 缓冲区用于读取客户端数据// 使用线程池处理,以免阻塞主线程workerPool.submit(() -> {try {int bytesRead = clientChannel.read(buffer); // 读取客户端数据if (bytesRead == -1) {disconnect(clientChannel); // 客户端关闭连接return;}lastActiveTime.put(clientChannel, System.currentTimeMillis()); // 更新最后活动时间buffer.flip(); // 准备读取缓冲区内容String messageContent = new String(buffer.array(), 0, bytesRead).trim();Message message = new Message(clientNames.get(clientChannel), messageContent);System.out.println(message.getSender() + ": " + message.getContent());broadcast(message, clientChannel); // 广播消息给其他客户端} catch (IOException e) {disconnect(clientChannel); // 处理异常情况下的客户端断开}});}// 处理客户端断开连接private void disconnect(SocketChannel clientChannel) {try {String clientName = clientNames.get(clientChannel);System.out.println("Disconnected: " + clientName);clientNames.remove(clientChannel); // 移除客户端信息lastActiveTime.remove(clientChannel); // 移除最后活动时间clientChannel.close(); // 关闭连接broadcast(new Message("System", "User " + clientName + " left the chat"), clientChannel);} catch (IOException e) {e.printStackTrace();}}// 广播消息给所有连接的客户端(除了消息发送者)private void broadcast(Message message, SocketChannel sender) {ByteBuffer buffer;try {buffer = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message)); // 将消息序列化为 JSON} catch (IOException e) {e.printStackTrace();return;}for (SelectionKey key : selector.keys()) {Channel targetChannel = key.channel();if (targetChannel instanceof SocketChannel && targetChannel != sender) { // 排除发送者SocketChannel clientChannel = (SocketChannel) targetChannel;try {clientChannel.write(buffer.duplicate()); // 写入消息} catch (IOException e) {disconnect(clientChannel); // 处理写入失败的情况}}}}// 定期检查客户端是否超时未响应,超时则断开连接private void startHeartbeatCheck() {heartbeatScheduler.scheduleAtFixedRate(() -> {long currentTime = System.currentTimeMillis();for (SocketChannel clientChannel : lastActiveTime.keySet()) {long lastActive = lastActiveTime.get(clientChannel);if (currentTime - lastActive > 60000) { // 如果超时 1 分钟System.out.println("Client timeout: " + clientNames.get(clientChannel));disconnect(clientChannel); // 断开超时客户端}}}, 10, 30, TimeUnit.SECONDS); // 每隔 30 秒执行一次}public static void main(String[] args) throws IOException {int port = 123456; // 定义端口号new EnhancedNioChatServer(port).start(); // 启动服务器}// 用于封装消息的内部类private static class Message {private String sender;private String content;public Message(String sender, String content) {this.sender = sender;this.content = content;}public String getSender() {return sender;}public String getContent() {return content;}}
}
解释一下
selector
和serverSocketChannel
:负责管理通道事件和连接。clientNames
和lastActiveTime
:用于存储客户端信息,确保记录和维护连接状态。heartbeatScheduler
:定时执行心跳检测任务,定期检查每个客户端的活动状态,断开超时连接。workerPool
:线程池用于异步处理每个客户端的消息读取操作。- 消息广播和心跳检测:使用 JSON 格式消息封装,消息广播会将消息发送给除发送者以外的所有客户端。
优化说明
-
心跳检测:
- 使用
ScheduledExecutorService
每隔 30 秒检查一次所有客户端的最后活跃时间,如果某客户端超过 1 分钟未发送消息,则认为其超时,断开连接。
- 使用
-
线程池处理读事件:
handleRead
方法中的 I/O 操作被提交到workerPool
线程池,避免阻塞主线程,实现并发处理。这样即使某个客户端 I/O 操作较慢,服务器也能及时处理其他客户端的请求。
-
使用 JSON 协议封装消息:
- 使用
Jackson ObjectMapper
将消息对象Message
转换为 JSON 字符串,并进行发送和接收,这样消息内容更加结构化,客户端可以通过 JSON 协议轻松解析消息内容。
- 使用
代码执行流程
- 启动服务器:初始化服务器和选择器,启动心跳检测任务。
- 连接和广播:每当有新客户端连接时,注册为读事件,并广播加入消息。读事件被分配到线程池中处理,消息被 JSON 序列化后广播到其他客户端。
- 心跳检测:定期检查客户端是否超时,断开长时间无响应的客户端。
- 断开连接:客户端断开连接或超时后,释放相关资源并广播退出消息。
这种优化使得服务器在高并发场景下更加健壮、灵活,并支持更精确的消息协议。
小结一下
epoll
的高效性主要得益于两点:
- 通过红黑树管理事件,实现事件的快速增删查改操作。
- 使用就绪队列将活跃事件和非活跃事件分离,大幅减少不必要的系统调用。
好了,关于 epoll 多路复用你学会了吗,原创不易,感谢支持,关注威哥爱编程,编程路上 V 哥与你一路同行。
相关文章:
细谈 Linux 中的多路复用epoll
大家好,我是 V 哥。在 Linux 中,epoll 是一种多路复用机制,用于高效地处理大量文件描述符(file descriptor, FD)事件。与传统的select和poll相比,epoll具有更高的性能和可扩展性,特别是在大规模…...

51c自动驾驶~合集4
我自己的原文哦~ https://blog.51cto.com/whaosoft/12413878 #MCTrack 迈驰&旷视最新MCTrack:KITTI/nuScenes/Waymo三榜单SOTA paper:MCTrack: A Unified 3D Multi-Object Tracking Framework for Autonomous Driving code:https://gi…...
回归预测 | MATLAB实现BO-BiGRU贝叶斯优化双向门控循环单元多输入单输出回归预测
要在MATLAB中实现BO-BiGRU(贝叶斯优化双向门控循环单元)进行多输入单输出回归预测,您需要执行以下步骤: 数据准备:准备您的训练数据和测试数据。 模型构建:构建BO-BiGRU模型,可以使用MATLAB中的…...

2-ARM Linux驱动开发-设备树平台驱动
一、概述 设备树(Device Tree)是一种描述硬件的数据结构,用于将硬件设备的信息传递给操作系统内核。它的主要作用是使内核能够以一种统一、灵活的方式了解硬件平台的细节,包括设备的拓扑结构、资源分配(如内存地址、中断号等)等信…...

C语言函数与递归
函数 函数是指将一组能完成一个功能或多个功能的语句放在一起的代码结构。在C语言程序中,至少会包含一个函数,主函数main()。本章将详细讲解关于函数的相关内容。 1、库函数 ⭕️C语言库函数是指在C语言标准库中预先定义好的函数,这些函数包…...

Linux下的Debugfs
debugfs 1. 简介 类似sysfs、procfs,debugfs 也是一种内存文件系统。不过不同于sysfs一个kobject对应一个文件,procfs和进程相关的特性,debugfs的灵活度很大,可以根据需求对指定的变量进行导出并提供读写接口。debugfs又是一个Li…...
【FFmpeg】调整音频文件的音量
1、调整音量的命令 1)音量调整为当前音量的十倍 ffmpeg -i inputfile -vol 1000 outputfile 2)音量调整为当前音量的一半 ffmpeg -i input.wav -filter:a "volume=0.5" output.wav3)静音 ffmpeg -i input.wav -filter:a "volume=0" output.wav4)…...
mac 打开访达快捷键
一、使用快捷键组合 1. Command N 在当前桌面或应用程序窗口中,按下“Command N”组合键可以快速打开一个新的访达窗口。这就像在 Windows 系统中通过“Ctrl N”打开新的资源管理器窗口一样。 2. Command Tab 切换 如果访达已经打开,只是被其他应…...

Ubuntu学习笔记 - Day2
文章目录 学习目标:学习内容:学习笔记:Linux系统启动过程内核引导运行init运行级别系统初始化建立终端用户登录系统 Ubuntu关机关机流程相关命令 Linux系统目录结构查看目录目录结构 文件基本属性读写权限命令 下载文件的方法安装wget工具下载…...
c++基础12比较/逻辑运算符
比较/逻辑运算符 布尔比较运算符逻辑运算符位运算符(也用于逻辑运算)1<a<10怎么表达T140399判断是否为两位数代码 布尔 在C中,布尔类型是一种基本数据类型,用于表示逻辑值,即真(true)或假…...

mac-ubuntu虚拟机(扩容-共享-vmtools)
一、磁盘扩容 使用GParted工具对Linux磁盘空间进行扩展 https://blog.csdn.net/Time_Waxk/article/details/105675468 经过上面的方式后还不够,需要再进行下面的操作 lvextend 用于扩展逻辑卷的大小,-l 选项允许指定大小。resize2fs 用于调整文件系统的…...
数学建模学习(135):使用Python基于WSM、WPM、WASPAS的多准则决策分析
1. 算法介绍 多标准决策分析(Multi-Criteria Decision Analysis, MCDA)是帮助决策者在复杂环境下做出合理选择的重要工具。WSM(加权和法)、WPM(加权乘积法)、WASPAS(加权和乘积评估法)是 MCDA 中的三种常用算法。它们广泛应用于工程、经济、供应链管理等多个领域,用于…...

VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
以最简单的以原子的跑马灯为例: 1、点击CtrlShiftP,输入setting,然后回车 2、输入Browse 3、点击下面C_Cpp > Default > Browse:Path里面添加你的工程路径 然后就可以愉快地跳转定义啦~ 希望对你有帮助,如果还不可以的话&a…...

设备管理网关(golang版本)
硬件设备:移远EC200A-CN LTE Cat 4 无线通信模块 操作系统:openwrt 技术选型:layui golang sqlite websocket 工程结构 界面展示 区域管理 设备管理 运行监控 系统参数 资源文件 版本信息...

Armv8的安全启动
目录 1. Trust Firmware 2. TF-A启动流程 3. TF-M启动流程 3.1 BL1 3.2 BL2 4.小结 在之前汽车信息安全 -- 再谈车规MCU的安全启动文章里,我们详细描述了TC3xx 、RH850、NXPS32K3的安全启动流程,而在车控类ECU中,我们也基本按照这个流程…...
冒泡排序、选择排序、计数排序、插入排序、快速排序、堆排序、归并排序JAVA实现
常见排序算法实现 冒泡排序、选择排序、计数排序、插入排序、快速排序、堆排序、归并排序JAVA实现 文章目录 常见排序算法实现冒泡排序选择排序计数排序插入排序快速排序堆排序归并排序 冒泡排序 冒泡排序算法,对给定的整数数组进行升序排序。冒泡排序是一种简单…...

SQL CASE表达式与窗口函数
CASE 表达式是一种通用的条件表达式,类似于其他编程语言中的if/else语句。 窗口函数类似于group by,但是不会改变记录行数,能扫描所有行,能对每一行执行聚合计算或其他复杂计算,并把结果填到每一行中。 1 CASE 表达式…...

基于SpringBoot的植物园管理小程序【附源码】
基于SpringBoot的植物园管理小程序 效果如下: 系统登录页面 管理员主页面 商品订单管理页面 植物园信息管理页面 小程序主页面 小程序登录页面 植物信息查询推荐页面 研究背景 随着互联网技术的快速发展和移动设备的普及,线上管理已经成为各行各业提高…...
asp.net网站项目如何设置定时器,定时获取数据
在 Global.asax.cs 文件中编写代码来初始化和启动定时器。Global.asax.cs 文件定义了应用程序全局事件,比如应用程序的启动和结束。在这里,我们将在应用程序启动时初始化和启动定时器。 using System; using System.Timers;public class Global : Syste…...

单元/集成测试解决方案
在项目开发的前期针对软件单元/模块功能开展单元/集成测试,可以尽早地发现软件Bug,避免将Bug带入系统测试阶段,有效地降低HIL测试的测试周期,也能有效降低开发成本。单元/集成测试旨在证明被测软件实现其单元/架构设计规范、证明被…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...

【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...