细谈 Linux 中的多路复用epoll

大家好,我是 V 哥。在 Linux 中,epoll 是一种多路复用机制,用于高效地处理大量文件描述符(file descriptor, FD)事件。与传统的selectpoll相比,epoll具有更高的性能和可扩展性,特别是在大规模并发场景下,比如高并发服务器。

以下是epoll的核心数据结构和实现原理:
V 哥推荐:2024 最适合入门的 JAVA 课程

1. epoll的核心数据结构

在 Linux 内核中,epoll的实现涉及多个核心数据结构,主要包括以下几个:

(1) epoll实例

epoll在创建时,会生成一个与之关联的实例,这个实例在内核中是一个epoll文件对象(struct file),并且与用户态的epoll文件描述符(FD)对应。该实例负责维护和管理所有加入的事件。

(2) 事件等待队列(epitem

epoll中的每个事件都被封装成一个epitem结构。该结构体主要包括以下几个关键内容:

  • 指向被监听文件的指针:用于标识监听的文件对象。
  • 事件类型和事件掩码:指定关注的事件类型(如可读、可写、异常等)。
  • 双向链表节点:用于将所有的epitem结构体组织成链表(或红黑树)。
(3) 红黑树(RB-Tree)

为了快速查找和管理epitemepoll使用红黑树将所有的epitem组织起来。每个被监听的文件描述符及其事件类型会存储在红黑树中,通过这种方式,可以在事件添加、删除、修改时实现高效的查找和管理。

(4) 就绪队列(Ready List)

当监听的文件描述符上发生指定的事件时,epoll会将该文件描述符的事件加入一个就绪队列。这个队列是一个双向链表,存储所有准备好处理的epitem。当用户调用epoll_wait时,内核从该队列中取出满足条件的事件并返回。

2. epoll的三种操作

epoll提供三种主要的操作接口:epoll_createepoll_ctlepoll_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只需遍历就绪队列中的事件,而不是遍历所有的监听事件,这使得性能相较于selectpoll有显著提升。特别是在大量文件描述符中仅有少数活跃时,epoll_wait的优势更为明显。

3. epoll的触发模式

epoll提供两种触发模式来控制事件的触发方式:

(1) 水平触发(LT, Level Triggered)

在默认的水平触发模式下,只要文件描述符上有指定的事件(如数据可读),每次调用epoll_wait都会返回此事件,除非事件被处理(如数据被读走)。这是与pollselect一致的行为。

(2) 边缘触发(ET, Edge Triggered)

在边缘触发模式下,epoll_wait只会在事件第一次发生时通知,之后即使该事件条件一直满足(如数据仍可读),也不会再次触发,除非事件条件有新的变化。该模式能够减少不必要的系统调用次数,但要求应用程序在接收到通知后必须一次性处理所有数据,否则可能会错过事件。

4. epoll的优缺点

优点:
  • 高效的事件监听:使用红黑树管理监听事件,提高了事件的增删查效率。
  • 事件驱动的高并发处理:通过边缘触发模式,减少系统调用次数,适合高并发场景。
  • 就绪事件分离:就绪队列与监听列表分离,不必遍历所有文件描述符,从而大大提升了性能。
缺点:
  • 只支持 Linuxepoll是 Linux 特有的实现,跨平台兼容性较差。
  • 编程复杂度:相比selectpollepoll需要更精细的控制,特别是在边缘触发模式下应用程序需要处理全部数据,以防止事件丢失。

5. Java NIO 如何使用多路复用

下面 V 哥用案例来详细说一说Java 中的多路复用。在 Java NIO 中,Selector 类实现了多路复用机制,底层使用 epollpoll 实现。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();}
}

代码说明

  1. 初始化服务器

    • 使用 ServerSocketChannel.open() 创建服务器套接字通道,配置为非阻塞模式,并绑定端口。
    • 使用 Selector.open() 创建选择器并将 ServerSocketChannel 注册到 Selector 上,监听连接事件 SelectionKey.OP_ACCEPT
  2. 事件处理

    • selector.select() 会阻塞直到至少一个通道变为就绪状态。
    • key.isAcceptable():处理新的客户端连接,将新客户端通道注册到选择器中,监听读取事件 SelectionKey.OP_READ
    • key.isReadable():读取来自客户端的消息并广播给所有其他客户端。
  3. 广播机制

    • 使用 Selector.keys() 遍历所有注册的通道(包含当前连接的所有客户端),将消息写入除发送者之外的所有客户端通道。

业务场景扩展

在实际业务中,可以进一步优化或扩展这个代码,比如:

  • 增加心跳检测来处理空闲客户端连接,避免资源浪费。
  • 将每个 SocketChannel 放到单独的线程池中处理,以实现更精细的并发控制。
  • 实现消息格式协议(如 JSON 或 Protobuf)来传输结构化数据。

6. 优化一下

在实际业务场景中,我们可以基于 Java NIO 对该聊天服务器进行如下优化:

  1. 心跳检测:定期检测客户端连接是否空闲,断开长时间无响应的连接,以节省资源。
  2. 线程池处理:将每个 SocketChannel 的消息处理放入线程池,以避免阻塞主线程,提高并发性能。
  3. 消息协议格式:使用 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;}}
}

解释一下

  1. selectorserverSocketChannel:负责管理通道事件和连接。
  2. clientNameslastActiveTime:用于存储客户端信息,确保记录和维护连接状态。
  3. heartbeatScheduler:定时执行心跳检测任务,定期检查每个客户端的活动状态,断开超时连接。
  4. workerPool:线程池用于异步处理每个客户端的消息读取操作。
  5. 消息广播和心跳检测:使用 JSON 格式消息封装,消息广播会将消息发送给除发送者以外的所有客户端。

优化说明

  1. 心跳检测

    • 使用 ScheduledExecutorService 每隔 30 秒检查一次所有客户端的最后活跃时间,如果某客户端超过 1 分钟未发送消息,则认为其超时,断开连接。
  2. 线程池处理读事件

    • handleRead 方法中的 I/O 操作被提交到 workerPool 线程池,避免阻塞主线程,实现并发处理。这样即使某个客户端 I/O 操作较慢,服务器也能及时处理其他客户端的请求。
  3. 使用 JSON 协议封装消息

    • 使用 Jackson ObjectMapper 将消息对象 Message 转换为 JSON 字符串,并进行发送和接收,这样消息内容更加结构化,客户端可以通过 JSON 协议轻松解析消息内容。

代码执行流程

  1. 启动服务器:初始化服务器和选择器,启动心跳检测任务。
  2. 连接和广播:每当有新客户端连接时,注册为读事件,并广播加入消息。读事件被分配到线程池中处理,消息被 JSON 序列化后广播到其他客户端。
  3. 心跳检测:定期检查客户端是否超时,断开长时间无响应的客户端。
  4. 断开连接:客户端断开连接或超时后,释放相关资源并广播退出消息。

这种优化使得服务器在高并发场景下更加健壮、灵活,并支持更精确的消息协议。

小结一下

epoll的高效性主要得益于两点:

  • 通过红黑树管理事件,实现事件的快速增删查改操作。
  • 使用就绪队列将活跃事件和非活跃事件分离,大幅减少不必要的系统调用。

好了,关于 epoll 多路复用你学会了吗,原创不易,感谢支持,关注威哥爱编程,编程路上 V 哥与你一路同行。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据转换 | Matlab基于SP符号递归图(Symbolic recurrence plots)一维数据转二维图像方法

目录 基本介绍程序设计参考资料获取方式 基本介绍 Matlab基于SP符号递归图&#xff08;Symbolic recurrence plots&#xff09;一维数据转二维图像方法 符号递归图(Symbolic recurrence plots)是一种一维时间序列转图像的技术&#xff0c;可用于平稳和非平稳数据集;对噪声具有…

特殊矩阵的压缩存储

一维数组的存储结构 ElemType arr[10]; 各数组元素大小相同&#xff0c;且物理上连续存放。 数组元素a[i]的存放地址 LOC i * sizeof(ElemType)。&#xff08;LOC为起始地址&#xff09; 二维数组的存储结构 ElemType b[2][4];二维数组也具有随机存取的特性&#xff08;需…

MySQL utf8mb3 和 utf8mb4引发的问题

问题描述 Cause: java.sql.SQLException: Incorrect string value: \xF4\x8F\xBB\xBF-b... for column sddd_aaa_ark at row 1 sddd_aaa_ark 存储中文字符时&#xff0c;出现上述问题 原因分析 sddd_aaa_ark在数据库中结构是 utf8字符的最大字节数是3 byte&#xff0c;但是某些…

RK3568开发板Openwrt文件系统构建

RK3568开发板Openwrt文件系统构建 iTOP-RK3568开发板使用教程更新&#xff0c;后续资料会不断更新&#xff0c;不断完善&#xff0c;帮助用户快速入门&#xff0c;大大提升研发速度。 本次更新内容为《iTOP-3568开发板文件系统构建手册》&#xff0c;对Openwrt文件系统的编译…

Linux之crontab使用

一&#xff0c;查看cron是否已经在运行 查看crontab的运行状态 sudo service cron statussystemctl status cron 开启crontab: sudo service cron startsudo service cron restart 二&#xff0c;编辑cron定时任务 crontab -e加入你自己的命令&#xff0c;定时跑脚本&a…

OpenEuler 使用ffmpeg x11grab捕获屏幕流,rtsp推流,并用vlc播放

环境准备 安装x11grab(用于捕获屏幕流)和libx264(用于编码) # 基础开发环境&x11grab sudo dnf install -y \autoconf \automake \bzip2 \bzip2-devel \cmake \freetype-devel \gcc \gcc-c \git \libtool \make \mercurial \pkgconfig \zlib-devel \libX11-devel \libXext…

矩阵的奇异值分解SVD

为了论述矩阵的奇异值与奇异值分解!需要下面的结论!

H5开发指南|掌握核心技术,玩转私域营销利器

随着互联网技术的不断发展和用户需求的日益增长&#xff0c;H5页面逐渐成为了企业和个人展示信息、吸引用户关注的重要手段。具有跨平台兼容性强、网页链接分享、更新迭代方便快捷、低开发成本、可搜索和优化、数据分析与追踪、灵活性与扩展性以及无需下载安装等特点。不仅可以…

Ubuntu Linux

背景 Ubuntu起源于南非&#xff0c;其名称“Ubuntu”来源于非洲南部祖鲁语或豪萨语&#xff0c;意为“人性”、“我的存在是因为大家的存在”&#xff0c;这体现了非洲传统的一种价值观。Ubuntu由南非计算机科学家马克沙特尔沃斯&#xff08;Mark Shuttleworth&#xff09;创办…

你适合哪种tiktok广告账户类型?

TikTok在广告营销方面的分类体系极为详尽。在开设广告账户时&#xff0c;根据不同的海外市场和商品类型&#xff0c;TikTok会有各自的开户标准。此外&#xff0c;广告主所开设的TikTok广告账户类型会直接影响其可投放的广告类型。在广告出价方面&#xff0c;广告主的营销目标不…

平衡者:陈欣的宇宙使命

第一章 异象初现 2145年&#xff0c;地球已经不再是人类唯一的家园。随着科技的飞速发展&#xff0c;人类在银河系内建立了多个殖民星球。然而&#xff0c;这些新世界的繁荣背后隐藏着一个巨大的危机——各个星球之间的资源分配不均&#xff0c;导致了严重的社会动荡和冲突。 …

《AI产品经理手册》——解锁AI时代的商业密钥

在当今这个日新月异的AI时代&#xff0c;每一位产品经理都面临着前所未有的挑战与机遇&#xff0c;唯有紧跟时代潮流&#xff0c;深入掌握AI技术的精髓&#xff0c;才能在激烈的市场竞争中独占鳌头。《AI产品经理手册》正是这样一部为AI产品经理量身定制的实战宝典&#xff0c;…

React第十三章(useTransition)

useTransition useTransition 是 React 18 中引入的一个 Hook&#xff0c;用于管理 UI 中的过渡状态&#xff0c;特别是在处理长时间运行的状态更新时。它允许你将某些更新标记为“过渡”状态&#xff0c;这样 React 可以优先处理更重要的更新&#xff0c;比如用户输入&#x…

使用wordcloud与jieba库制作词云图

目录 一、WordCloud库 例子&#xff1a; 结果&#xff1a; 二、Jieba库 两个基本方法 jieba.cut() jieba.cut_for_serch() 关键字提取&#xff1a; jieba.analyse包 extract_tags() 一、WordCloud库 词云图&#xff0c;以视觉效果提现关键词&#xff0c;可以过滤文本…

2024年云手机推荐榜单:高性能云手机推荐

无论是手游玩家、APP测试人员&#xff0c;还是数字营销工作者&#xff0c;云手机都为他们带来了极大的便利。本文将为大家推荐几款在市场上表现优异的云手机&#xff0c;希望这篇推荐指南可以帮助大家找到最适合自己的云手机&#xff01; 1. OgPhone云手机 OgPhone云手机是一款…

Template Method(模板方法)

1)意图 定义一个操作中的算法骨架&#xff0c;而将一些步骤延迟到子类中。Template Method 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 2)结构 模板方法模式的结构图如图7-47 所示。 其中: AbstractClass(抽象类) 定义抽象的原语操作&#xff0c;具体…

自研小程序-心情追忆

在近期从繁忙的工作中暂时抽身之后&#xff0c;我决定利用这段宝贵的时间来保持我的Java技能不致生疏&#xff0c;并通过一个个人项目来探索人工智能的魅力。 我在Hugging Face&#xff08;国内镜像站点&#xff1a;HF-Mirror&#xff09;上发现了一个关于情感分析的练习项目&…

【设计模式】策略模式定义及其实现代码示例

文章目录 一、策略模式1.1 策略模式的定义1.2 策略模式的参与者1.3 策略模式的优点1.4 策略模式的缺点1.5 策略模式的使用场景 二、策略模式简单实现2.1 案例描述2.2 实现代码 三、策略模式的代码优化3.1 优化思路3.2 抽象策略接口3.3 上下文3.4 具体策略实现类3.5 测试 参考资…

【React】初学React

A. react中如何创建元素呢&#xff1f; 说明一点&#xff1a; 属性都改为驼峰形式&#xff08;无障碍属性aria-*除外&#xff09;&#xff0c; class改成className 创建元素 B. 变量或表达式如何表示呢&#xff1f;大括号{ }包起来 变量值用大括号包裹 C. 元素和组件的区别 元素…

伦敦金价格是交易所公布的吗?

今年以来&#xff0c;伦敦金价格波动可谓是波澜壮阔&#xff0c;盘中屡次刷新历史新高&#xff0c;目前已经冲上了2700的历史大关。面对高歌猛进的伦敦金价格&#xff0c;投资者除了进行交易之外&#xff0c;还有一点相关方面的知识是想了解的。例如&#xff0c;伦敦金价格是交…