好的,我们来深入剖析陈硕老师开发的著名C++网络库——muduo。它以“简单、高效、易用”著称,是学习Linux C++高性能网络编程的绝佳范本。我会尽量详细、通俗地讲解其核心思想、关键组件、源码结构和工作原理。

核心思想:Reactor 模式 (Non-blocking + I/O Multiplexing)

muduo 的灵魂是 Reactor 模式。理解它就理解了 muduo 的一半。想象一下:

  1. 传统阻塞模型的问题: 想象一个餐厅只有一个服务员。每次点菜、上菜、结账,服务员都要等顾客做完一件事才能服务下一个(阻塞)。效率极低,顾客(连接)一多就完蛋。

  2. Reactor 模型的解决方案: 餐厅安装了一个呼叫铃系统(epoll/poll/kqueue)。服务员(主线程)只需要坐在前台,盯着一个大屏幕(事件循环 EventLoop),哪个桌子的铃响了(文件描述符 fd 就绪了),服务员就去处理哪个桌子的需求(回调函数)。服务员永远不会傻等

  3. 关键点:

    • 非阻塞 I/O (Non-blocking I/O): 所有网络操作(acceptreadwriteconnect)都设置成非阻塞。调用它们会立即返回,如果数据没准备好(比如 read 时缓冲区空),就返回一个错误(EAGAIN 或 EWOULDBLOCK),而不是傻等。

    • I/O 多路复用 (I/O Multiplexing): 使用 epoll(Linux 首选)、poll 或 select(效率低,不推荐)来同时监听大量文件描述符(fd)上的事件(可读、可写、错误等)。当任何一个被监听的 fd 上有事件发生时,多路复用器会通知程序。

    • 事件驱动 (Event-Driven): 程序的核心是一个事件循环 (Event Loop)。它不断地询问多路复用器:“哪些 fd 有事件了?”。然后,它根据 fd 上发生的事件类型(读、写),调用预先注册好的回调函数 (Callback) 来处理这些事件(比如读取数据、发送数据、接受新连接)。

muduo 的主要实现方法和核心组件

muduo 将 Reactor 模式拆解并封装成几个核心类,它们协同工作:

  1. EventLoop (事件循环): 这是 Reactor 模式的心脏和发动机。

    • 职责: 每个 EventLoop 对象在一个线程中运行,负责不断执行以下任务:

      • 调用 Poller 获取就绪的事件列表。

      • 遍历就绪事件列表。

      • 根据事件关联的 Channel 对象,调用相应的读/写回调函数。

      • 处理定时器到期事件。

      • 执行其他线程通过 runInLoop 提交过来的函数(跨线程调用)。

    • 关键成员:

      • Poller* poller_:指向具体的 I/O 多路复用器实现(EPollPoller 或 PollPoller)。

      • ChannelList activeChannels_:存放本次循环中有事件发生的 Channel

      • TimerQueue timerQueue_:管理定时器。

      • int wakeupFd_ + Channel wakeupChannel_:用于唤醒阻塞在 Poller::poll() 上的事件循环(例如其他线程有任务要提交)。

    • 源码关键方法 (loop.cc):

      • loop():核心循环函数,调用 poll -> 填充 activeChannels_ -> 处理每个 Channel 的事件 (handleEvent) -> 处理定时器 -> 执行 pendingFunctors_

      • runInLoop(const Functor& cb)queueInLoop(const Functor& cb):安全地跨线程向 EventLoop 提交任务。

      • updateChannel(Channel*)removeChannel(Channel*):管理 Poller 监听的 Channel

  2. Channel (通道): 事件分派器。它是 EventLoop 与具体文件描述符之间的桥梁。

    • 职责: 封装一个文件描述符 (fd) 及其感兴趣的事件 (读、写等) 和 事件发生时的回调函数。它是事件处理的最小单位

    • 关键成员:

      • int fd_:它负责的文件描述符(socket, eventfd, timerfd, signalfd 等)。

      • int events_:它关心的事件(EPOLLINEPOLLOUTEPOLLPRIEPOLLERREPOLLHUP)。

      • int revents_:由 Poller::poll() 设置,表示 fd_ 上实际发生的事件。

      • ReadEventCallback readCallback_:可读事件回调。

      • EventCallback writeCallback_:可写事件回调。

      • EventCallback closeCallback_:关闭事件回调。

      • EventCallback errorCallback_:错误事件回调。

      • EventLoop* loop_:它所属的 EventLoop

    • 源码关键方法 (Channel.cc):

      • handleEvent(Timestamp receiveTime):核心方法!被 EventLoop::loop() 调用。根据 revents_ 的值,判断发生了什么事件(读?写?错误?关闭?),然后调用对应的回调函数。这是事件处理的最终落脚点。

      • enableReading()enableWriting()disableWriting()disableAll():设置/修改 events_,并调用 update() 通知 EventLoop 更新 Poller 的监听。

      • update():内部调用 EventLoop::updateChannel(this)

  3. Poller (轮询器): I/O 多路复用的抽象层。

    • 职责: 封装底层 I/O 多路复用系统调用(epoll_waitpoll)。负责监听一组 Channel(通过其 fd_ 和 events_),并在事件发生时填充 revents_ 并返回有事件发生的 Channel 列表给 EventLoop

    • 多态实现:

      • EPollPoller (Linux 首选):使用高效的 epoll

      • PollPoller:使用传统的 poll(效率较低,作为备选)。

    • 关键成员 (EPollPoller.cc):

      • int epollfd_epoll_create 创建的描述符。

      • std::vector epoll_events_:存放 epoll_wait 返回的就绪事件。

    • 源码关键方法:

      • poll(int timeoutMs, ChannelList* activeChannels):调用 epoll_wait/poll,将就绪的事件对应的 Channel 找出,设置其 revents_,并放入 activeChannels 列表返回给 EventLoop

      • updateChannel(Channel*)removeChannel(Channel*):向 epollfd_ 添加、修改或删除对某个 fd (Channel) 的监听。

  4. Acceptor (接受器): 专门处理新连接接入。

    • 职责: 封装监听套接字 (listening socket) 的 Channel。当监听套接字可读(有新连接到来)时,调用其回调函数 handleRead()。在 handleRead() 中,调用 accept 接受新连接,然后调用用户注册的 NewConnectionCallback (通常是 TcpServer 提供的)。

    • 位置: Acceptor 通常由 TcpServer 拥有和使用。

    • 源码关键方法 (Acceptor.cc):

      • handleRead():核心方法。调用 accept 获取新连接的 connfd,创建 InetAddress 表示客户端地址,然后调用 newConnectionCallback_(connfd, peerAddr)

  5. TcpConnection (TCP 连接): 已建立连接的抽象。这是用户与网络交互的核心对象。

    • 职责: 封装一个已建立的 TCP 连接的生命周期。它包含:

      • 该连接对应的 Socket 对象 (封装 connfd)。

      • 该连接对应的 Channel 对象 (用于在 EventLoop 中监听 connfd 的事件)。

      • 输入缓冲区 (inputBuffer_): 应用层接收缓冲区。当 Channel 的可读回调被调用时,从 connfd 读取数据追加到 inputBuffer_,然后调用用户设置的 MessageCallback。用户处理的是 inputBuffer_ 里的数据。

      • 输出缓冲区 (outputBuffer_): 应用层发送缓冲区。用户调用 send 或 write 时,数据先写入 outputBuffer_。如果 connfd 当前可写,则尝试直接从 outputBuffer_ 向内核发送数据;如果内核发送缓冲区满(write 返回 EAGAIN)或 outputBuffer_ 还有数据没发完,则通过 Channel 监听 EPOLLOUT 事件。当可写事件发生时,继续尝试发送 outputBuffer_ 中的数据,发完后取消监听 EPOLLOUT

      • 各种回调函数 (ConnectionCallbackMessageCallbackWriteCompleteCallbackCloseCallback):由用户设置,在连接建立、收到消息、数据发送完成、连接关闭时被调用。

    • 核心思想 - 缓冲区 (Buffer): muduo 采用 应用层缓冲区 是高性能网络库的关键设计。它解耦了网络 I/O 的速率与用户处理逻辑的速率inputBuffer_ 允许 TCP 粘包处理由用户决定;outputBuffer_ 允许用户在任何时候调用 send(即使内核缓冲区暂时不可写),避免阻塞用户线程。

    • 源码关键方法 (TcpConnection.cc):

      • handleRead(Timestamp)Channel 的可读回调。从 socket_ 读取数据到 inputBuffer_,调用 messageCallback_

      • handleWrite()Channel 的可写回调。尝试将 outputBuffer_ 中的数据写入 socket_。如果写完了,取消监听 EPOLLOUT,调用 writeCompleteCallback_;如果没写完,继续监听 EPOLLOUT

      • handleClose()handleError():处理关闭和错误。

      • send(const void* message, size_t len)send(const StringPiece& message)send(Buffer*):用户发送数据的接口。核心逻辑是将数据放入 outputBuffer_,然后尝试立即发送(如果 Channel 没有在监听 EPOLLOUT 且 outputBuffer_ 之前为空),否则会触发后续的 handleWrite

      • shutdown()forceClose():关闭连接。

  6. TcpServer (TCP 服务器): 管理整个服务器生命周期。

    • 职责: 组合上述组件,提供用户友好的服务器接口。

      • 持有 Acceptor 对象监听新连接。

      • 持有 EventLoopThreadPool 线程池(可选)。

      • 管理所有存活的 TcpConnection (std::map)。

      • 设置各种回调 (ConnectionCallbackMessageCallback 等) 并传递给新建的 TcpConnection

    • 多线程模型 (EventLoopThreadPool):

      • IO线程: 运行 EventLoop 的线程。负责处理 I/O 事件(acceptreadwrite)。TcpServer 的 EventLoop (通常叫 baseloop_) 运行 Acceptor。新建的 TcpConnection 的 EventLoop 由线程池分配。

      • 计算线程池 (可选): 如果业务逻辑计算密集,用户可以在 MessageCallback 中将接收到的数据 inputBuffer_ 传递给计算线程池处理,处理完后再通过 runInLoop 将结果交还给该连接的 IO 线程,通过 TcpConnection::send 发送。IO 线程只做 I/O,计算线程只做计算,避免计算阻塞 I/O。

    • 源码关键方法 (TcpServer.cc):

      • start():启动服务器。启动线程池(如果设置了),让 Acceptor 开始监听。

      • newConnection(int sockfd, const InetAddress& peerAddr)Acceptor 的 NewConnectionCallback。创建 TcpConnection 对象,选择一个 EventLoop (IO线程) 管理它,设置好各种回调,并加入到 connectionMap_

      • removeConnection(const TcpConnectionPtr& conn)TcpConnection::CloseCallback。从 connectionMap_ 移除连接。注意: 移除操作必须在 conn 所属的 IO 线程中执行(通过 runInLoop 保证)。

  7. Buffer (缓冲区): 应用层缓冲区,核心数据结构。

    • 设计: muduo::net::Buffer 是一个非线程安全的、基于 std::vector 的动态增长缓冲区。它采用 “读指针”和“写指针” (内部用索引实现) 的设计,避免频繁的内存拷贝。

    • 内存布局:

      text

      [Prependable Bytes] [Readable Bytes] [Writable Bytes]
      |                 |                |               |
      0           readerIndex_   writerIndex_      size()
      • Prependable Bytes: 预留空间,方便在数据前面添加头部(如长度字段)。

      • Readable Bytes: readerIndex_ 到 writerIndex_ 之间的数据,是待用户读取/处理的有效数据 (inputBuffer_) 或待发送的数据 (outputBuffer_)。

      • Writable Bytes: writerIndex_ 到 size() 之间的空间,可写入新数据。

    • 关键操作 (Buffer.cc):

      • retrieve(size_t len):用户读取了 len 字节后调用,移动 readerIndex_

      • retrieveAll():移动 readerIndex_ 和 writerIndex_ 到初始位置(可能回收内存)。

      • append(const char* data, size_t len)append(const void* data, size_t len):向 Writable 区域写入数据,移动 writerIndex_

      • prepend(const void* data, size_t len):向 Prependable 区域写入数据,移动 readerIndex_ (向前)。

      • readFd(int fd, int* savedErrno):核心!从 fd 读取数据到 Buffer 的 Writable 区域。如果空间不够,Buffer 会自动扩容。使用 readv 系统调用进行分散读 (Scatter Read),先读到 Buffer 的 Writable 空间,如果不够,再读到栈上的临时缓冲区,最后 append 到 Buffer。高效地利用了内存和系统调用。

      • writeFd(int fd, int* savedErrno):核心!将 Readable 区域的数据写入 fd。使用 write 系统调用。

muduo 的设计哲学与优势

  1. One Loop Per Thread + ThreadPool:

    • 每个 IO 线程运行一个 EventLoop

    • Acceptor 在 main loop 中。

    • 新连接 TcpConnection 被分配到某个 IO loop

    • 计算任务交给单独的线程池。

    • 优点: 充分利用多核;避免锁竞争(每个连接的数据只在其 IO 线程内操作);结构清晰。

  2. Non-Blocking + Buffer:

    • 所有 I/O 操作都是非阻塞的。

    • 使用应用层缓冲区 (Buffer) 解耦 I/O 速率与处理速率,这是高性能的关键。

  3. 基于事件回调 (Event Callback):

    • 通过函数对象 (std::function) 实现高度灵活性。用户只需注册关心的回调函数。

  4. 资源管理:shared_ptr + weak_ptr

    • TcpConnection 的生命期由 shared_ptr 管理。当 Channel 触发关闭事件时,TcpConnection 的回调最终会将其从 TcpServer 的 connectionMap_ 中移除并销毁。weak_ptr 用于跨线程安全地访问 TcpConnection

  5. RAII (Resource Acquisition Is Initialization):

    • 大量使用 RAII 管理资源(文件描述符 Socket、内存、锁 MutexLockGuard),确保异常安全。

  6. 简单即美:

    • 避免过度设计。核心类职责明确,接口清晰。源码相对容易阅读(对于 C++ 网络库而言)。

如何使用 muduo (一个简单 EchoServer 示例)

cpp

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/base/Logging.h>using namespace muduo;
using namespace muduo::net;void onConnection(const TcpConnectionPtr& conn) {if (conn->connected()) {LOG_INFO << "New Connection: " << conn->peerAddress().toIpPort();} else {LOG_INFO << "Connection Closed: " << conn->peerAddress().toIpPort();}
}void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time) {// 接收到的数据在 buf 中string msg(buf->retrieveAllAsString()); // 取出所有数据LOG_INFO << "Received " << msg.size() << " bytes from " << conn->peerAddress().toIpPort();conn->send(msg); // 原样发回 (Echo)
}int main() {EventLoop loop; // Main EventLoopInetAddress listenAddr(8888);TcpServer server(&loop, listenAddr, "EchoServer");// 设置回调函数server.setConnectionCallback(onConnection);server.setMessageCallback(onMessage);server.start(); // 启动监听loop.loop();    // 启动事件循环 (阻塞在此)return 0;
}

剖析一下这个例子如何映射到 muduo 组件:

  1. EventLoop loop;:创建主事件循环。

  2. TcpServer server(...);:创建 TcpServer

    • 内部创建 Acceptor 监听端口 8888。

    • Acceptor 的 Channel 注册到 loop 上监听 EPOLLIN (新连接)。

  3. server.setXxxCallback():设置用户回调。

  4. server.start():启动 Acceptor 开始监听。

  5. loop.loop():启动事件循环。

    • 当有新连接 (Acceptor 的 Channel 可读),Acceptor::handleRead() 被调用 -> accept -> 创建 TcpConnection 对象 conn -> 选择一个 IO loop -> 在该 IO loop 中注册 conn 的 Channel -> 设置 conn 的回调 (onConnectiononMessage) -> 将 conn 加入 TcpServer 的管理 map。

    • 当 conn 上有数据到来 (conn 的 Channel 可读),TcpConnection::handleRead() 被调用 -> 读入 inputBuffer_ -> 调用用户 onMessage(conn, inputBuffer_, time) -> 用户在 onMessage 中处理数据 (buf->retrieveAllAsString()) 并调用 conn->send(msg) -> send 将数据放入 outputBuffer_ 并尝试立即发送或注册 EPOLLOUT

    • 当 conn 可写时 (EPOLLOUT 触发),TcpConnection::handleWrite() 被调用 -> 发送 outputBuffer_ 中的数据。

源码阅读建议

  1. 从示例开始: 先编译运行 examples 目录下的简单例子 (如 echodiscardchargen),感受用法。

  2. 核心类入手: 重点阅读 EventLoopChannelPoller (EPollPoller), TcpConnectionBuffer 的实现。理解它们的关系和协作流程 (loop() -> poll() -> handleEvent() -> readCallback_/writeCallback_ -> Buffer 操作)。

  3. 关注回调注册与触发: 在 TcpServerAcceptorTcpConnection 中,看回调 (std::function) 是如何被设置,并在何时被调用的。

  4. 理解 Buffer 的设计: 仔细看 Buffer::readFd 和 Buffer::writeFd 的实现,理解其高效性。

  5. 多线程模型: 研究 EventLoopThreadEventLoopThreadPool 以及 TcpServer 如何分配新连接。注意跨线程调用的 runInLoop 机制和 wakeupFd_ 的作用。

  6. RAII 与智能指针: 观察 Socket 类如何管理 fdTcpConnection 的生命期如何通过 shared_ptr 管理,Channel 如何安全地从 EventLoop 移除。

总结

muduo 是一个将 Reactor 模式在 Linux C++ 环境下实现得精炼、高效且实用的网络库。其核心在于:

  • 事件驱动: EventLoop + Poller + Channel 构成了事件处理引擎。

  • 非阻塞 I/O + 应用层缓冲区: TcpConnection + Buffer 高效处理连接数据流。

  • 清晰的线程模型: One Loop Per Thread + ThreadPool 平衡了并发与复杂度。

  • 基于回调的编程模型: 用户只需关注连接、数据到达、数据发送完成等事件的处理逻辑。

  • RAII 与智能指针: 确保资源安全和简化生命周期管理。

深入理解 muduo 的源码,不仅对使用它大有裨益,更是学习 Linux 高性能服务器编程思想、C++ 网络编程实践和良好软件设计模式的宝贵资源。祝你学习顺利!

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

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

相关文章

将目录下所有图像中非0像素值改为1或者255

图像二值化处理技术大纲 目标与背景 解释图像二值化的意义,分析将非零像素值统一调整为1或255的应用场景(如简化数据、增强特征、适配模型输入等)。 核心方法概述 列举常见图像格式(如PNG、JPEG)的像素值范围,说明非零像素的定义(RGB或灰度图像中的非黑像素)。 方…

Reactor ConnectableFlux支持多订阅者

在 Reactor 中&#xff0c;ConnectableFlux 是一种用于处理响应式流的机制&#xff0c;它允许你控制何时开始订阅和数据生成。通常情况下&#xff0c;订阅者&#xff08;subscriber&#xff09;在订阅时会立即开始接收数据&#xff0c;但有时你可能希望多个订阅者“会面”&…

vite + vue 项目下使用 tailwindcss

版本 node: > 18.0.0 vue: 3.5.13 vite: 6.3.1 tailwindcss: 4.1.6 tailwindcss/vite: 4.1.6 tailwindcss ✅ 细粒度类库 提供数千个原子级CSS类&#xff08;如 text-center、bg-blue-500、p-4&#xff09;&#x1f9e9; 组合式开发 通过类名组合构建完全自定义的UI&#x…

Hibernate中save与saveOrUpdate的差异解析

在Hibernate中&#xff0c;save()和saveOrUpdate()都是用于持久化对象的方法&#xff0c;但它们的适用场景和行为有显著差异&#xff1a; 1. save()方法 核心行为&#xff1a; 仅适用于瞬时态&#xff08;Transient&#xff09;对象&#xff08;即新创建、未与Session关联的对象…

香橙派3B学习笔记14:deb 打包程序_解包前后脚本运行

本文学习如何用deb打包的方式打包自己需要调用系统库的程序。 然后实现deb解包前后的脚本运行。 目录 承接上文&#xff1a; 删除上文遗留的.so文件&#xff1a; 终止ledlight进程&#xff1a; 目标解释&#xff1a; 创建项目结构&#xff1a; 创建control文件&#xff1a; 创…

nanoGPT复现——prepare拆解(自己构建词表 VS tiktoken)

在nanoGPT的data文件夹有两个很相似的文件夹结构&#xff1a;shakespeare和shakespeare-char&#xff0c;这两种都是对shakespeare数据集的处理&#xff0c;但是shakespeare使用的是tiktoken对文字进行编码&#xff0c;另一个则是使用自己构建的词表 一、shakespeare-char&…

macos 安装 xcode

在 macOS 上安装 Xcode&#xff08;或者 Xcode Command Line Tools&#xff09;的方法如下&#xff1a; 1. 安装 Xcode Command Line Tools&#xff08;轻量级&#xff0c;满足大部分编译需求&#xff09; 终端命令&#xff1a; xcode-select --install会弹出安装提示&#x…

大学专业科普 | 云计算、大数据

大数据专业是近年来随着信息技术发展而兴起的热门学科&#xff0c;专注于从海量、多样化的数据中提取有价值信息&#xff0c;为各行业提供数据驱动的决策支持。 专业定义 大数据专业旨在培养掌握大数据采集、存储、管理、分析和应用等核心技术的人才。该专业融合了计算机科学…

本地文件自动提交到仓库

背景 将本地目录做一个存储仓库&#xff0c;将归档的文件放入其中。自动同步到远程仓库。 仓库配置 省略 配置密钥 用户可以 git pull \ git push \ git commit 自动 拉取、更新 脚本 文件名&#xff1a;autosave.sh #!/bin/zsh# 设置变量 LOCAL_DIR$1# 进入工作目录 cd "…

Ubuntu中控制用户存储空间配置步骤

目的&#xff0c;限制用户磁盘空间占用&#xff0c;例如给用户限制100-150G容量 1.安装磁盘配额工具 sudo apt-get install -y quota 2.备份并修改/etc/fstab文件&#xff0c;使能支持quota sudo cp /etc/fstab /etc/fstab.bak vim /etc/fstab #写入如下,usrjquotaaquota.u…

【网络】Linux 内核优化实战 - net.ipv4.tcp_rmem 和 net.core.rmem_default 关系

net.ipv4.tcp_rmem 和 net.core.rmem_default 都是 Linux 内核中控制网络接收缓冲区的参数,但它们的作用范围、优先级和使用场景存在明显区别。以下是详细对比: 核心区别 参数net.ipv4.tcp_rmemnet.core.rmem_default作用协议仅针对 TCP 协议针对 所有网络协议(TCP、UDP 等…

设计模式精讲 Day 14:命令模式(Command Pattern)

【设计模式精讲 Day 14】命令模式&#xff08;Command Pattern&#xff09; 文章内容 在“设计模式精讲”系列的第14天&#xff0c;我们来学习命令模式&#xff08;Command Pattern&#xff09;。命令模式是一种行为型设计模式&#xff0c;它将请求封装为对象&#xff0c;从而…

手机射频功放测试学习(二)——手机线性功放的静态电流和小信号(S-Parameter)测试

目录 一、概要 二、LPA的电流测试 1、LPA的泄漏电流测试 手动测试步骤如下: 自动化测试: 2、LPA的静态电流测试 手动测试步骤如下: 自动化测试: 三、LPA的S-Parameter测试 1、矢量网络分析仪校准 2、LPA的S参数手动测试步骤: 3、LPA的S参数自动测试步骤: 四…

基础算法合集-图论

本文将介绍数据结构图论部分中常见的算法 单源最短路径问题(用来计算一个点到其他所有顶点的最短路径) Dijkstra(n*n) 1. 初始化: 先找出从源点V0到各终点Vk的直达路径(V0,Vk), 即通过一条弧到达的路径 2. 选择: 从这些路径中找出一条长度最短的路径(V0,u) 3. 更新: 然后对其余…

vue-i18n 插件打包解析失效问题记录

vue-i18n 插件打包解析失效问题记录 开发环境中没有问题的&#xff0c;但打包发布之后就不行了&#xff0c;显示的就是模板字符串 // An highlighted block const messages {en: {step: {stepDesc1: Scan,stepDesc2: Analyze,stepDesc3: Result}},zh: {step: {stepDesc1: 扫描…

数据可视化 - 单子图

一、认识单子图 import matplotlib.pyplot as plt import numpy as np import pandas as pdplt.figure(num单子图, figsize(12, 8), facecolorw) # 中文字体 plt.rcParams[font.sans-serif] KaiTi # 负号显示 plt.rcParams[axes.unicode_minus] False# 2行&#xff0c;1列&a…

服务器上设置了代理之后,服务器可以访问外网,但是不能访问服务器本地。如何解决

你在服务器上设置了代理后&#xff0c;发现&#xff1a; 可以访问外网不能访问服务器本地地址&#xff08;如 localhost、127.0.0.1、内网IP&#xff09; 这是代理设置中常见的问题&#xff0c;尤其是当你设置了全局 HTTP/HTTPS 代理时。本地访问也会被强制走代理&#xff0c…

mysql启动报错:Can‘t connect to local MySQL server through socket

文章目录 一、报错内容二、解决方法 一、报错内容 在linux上启动mysql时报错 [rootlocalhost bin]# ./mysql -u root -p Enter password: ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)执行以上命令后报错&#xff0c;并且也…

C# Avalonia 绑定模式 Mode 的区别,它们的应用场景

C# Avalonia 绑定模式 Mode 的区别&#xff0c;它们的应用场景 文章目录 1. **Default&#xff08;默认模式&#xff09;**2. **OneTime&#xff08;一次性绑定&#xff09;**3. **OneWay&#xff08;单向绑定&#xff09;**4. **TwoWay&#xff08;双向绑定&#xff09;**5. *…

【OpenGL学习】(七)纹理单元

【OpenGL学习】&#xff08;七&#xff09;纹理单元 OpenGL的纹理单元&#xff08;Texture Unit&#xff09;是GPU中用于管理和组织纹理资源的逻辑单元&#xff0c;它允许开发者在渲染过程中同时使用多个纹理&#xff0c;并通过采样器&#xff08;Sampler&#xff09;在着色器…