深入理解网络 I/O 多路复用:Epoll

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • Epoll 函数
    • EPOLL_CREATE
    • EPOLL_CTL
    • EPOLL_WAIT
    • epoll_event 数据结构
    • 边沿触发、水平触发
    • 小结
  • Epoll 内核源码
  • 图解分析
    • epoll VS select/poll 工作原理
    • epoll VS select/poll 日志追踪
    • Epoll 优势之处
  • 总结

前言

Unix/Linux 下可用的 I/O 模型有以下五种:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select、poll)
  4. 信号驱动式 I/O(SIGIO)
  5. 异步 I/O

在 Linux 中操作内核时,所有的无非三种操作,分别是输入、输出、报错输出

0-输入
1-输出
2-报错输出

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字(Socket)的输入操作,第一步通常涉及等待数据从网络中;当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区

Epoll 函数

在 Epoll 多路复用模型中,最主要的是涉及到了三个系统函数指令,分别是:epoll_create、epoll_ctl、epoll_wait

EPOLL_CREATE

借助:man 2 epoll_create 帮助文档来学习该函数

通过 epoll_create、epoll_create1 函数,打开 epoll 文件描述符

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create 返回指向新的 Epoll 实例的文件描述符,该文件描述符用于对 epoll 接口的所有后续调用,当不再需要时,将调用 close 函数关闭 epoll_create 返回的文件描述符,当引用 epoll 实例的所有文件描述符都关闭时,内核将销毁该实例并释放相关资源以供资源重用

epoll_create1:若 flags 为 0,除了删除过时的 size 参数之外,epoll_create1 与 epoll_create 是相同的

如果指向新的 epoll 实例描述符成功的话,这些系统调用函数将会返回一个非负数的文件描述符,若出现错误,将返回 -1,并设置 errno 来指示错误.

EPOLL_CTL

借助:man 2 epoll_ctl 帮助文档来学习该函数

通过 epoll_ctl 来承担 epoll 描述符的控制接口

int epoll_ctl(int epfd, int op, 
				int fd, struct epoll_event *event);

这个系统调用对文件描述符 epfd 引用的 epoll 实例执行控制操作,它请求对目前文件描述符 fd 执行 op 操作

op 参数可选值如下:

  1. EPOLL_CTL_ADD:在文件描述符 epfd 引用的 epoll 实例上注册目标文件描述符 fd,并关联事件,内部文件链接到 fd
  2. EPOLL_CTL_MOD:更改与目标文件描符 fd 关联的事件
  3. EPOLL_CTL_DEL:从 epfd 引用的 epoll 实例中删除或注销目标文件描述符 fd,该事件被忽略,并且可以为空

EPOLL_WAIT

借助:man 2 epoll_wait 帮助文档来学习该函数

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

epoll_wait 系统调用等待文件描述符 epfd 引用的 epoll 实例,事件指向的内存区域将包含调用者可用的事件,epoll_wait 最多返回 maxevents 个事件,其参数必须大于 0

timeout 函数指定 epoll_wait 将阻塞的最小毫秒数,若指定 timeout 为 -1 会导致 epoll_wait 无限期阻塞,而指定 timeout 为 0 会导致 epoll_wait 立即返回,即使没有事件可用

在 epoll_wait 调用时,在给定的 timeout 时间内,当在监控的所有 fd 中有事件发生时,就返回用户态的进程

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 中重要返回的数据结构,每个返回结构的数据将包含用户使用:epoll_ctl「EPOLL_CTL_ADD、EPOLL_CTL_MOD」设置的相同数据,而 events 成员将包含返回的事件位字段

边沿触发、水平触发

Epoll 提供两种事件接口:边沿触发(ET:edge-triggered)、水平触发(LT:level-triggered)

边沿触发

  • socket 接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
  • socket 发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

水平触发

  • socket 接收缓冲区不为空时,有数据可读,读事件一直触发
  • socket 发送缓冲区不满,可以继续写入数据,写事件一直触发

使用 EPOLLET 标志的应用程序应该使用非阻塞文件描述符,以避免在处理多个文件描述符的任务中出现阻塞读写。建议使用 epoll 作为边缘触发(EPOLLET)接口的方法如下:

  1. 使用非阻塞文件描述符
  2. 在 read 或 write 之后等待事件返回 EAGAIN

例如,两条线分别有数据 ABC、DEF,水平触发的处理顺序:ADBECF,边缘触发的处理顺序:ABCDEF

Nginx、Redis 都使用了 Epoll 多路复用模型

Nginx 使用的是边缘触发 ET、Redis 使用的是水平触发 LT

再举例而言说明两者的区别:你有急事打电话找人,如果对方一直不接,那你只有一直打,直到他接电话为止,这就是 LT 模式;如果不急,电话打过去对方不接,那就等有空再打,这就是 ET 模式

小结

从调用方式可以看出 epoll 对比 select/poll 优越之处,因为 每次调用时都要传递你所要监控的所有 socket 给 select/poll 进行系统调用,这也就意味着需要将用户态的 socket 列表 copy 到内核态,若以万计数的 socket 会导致每次都要 copy 几十或几百 KB 的内存到达内核态,非常的低效,而当我们调用 epoll_wait 时就相当于以往调用 select/poll,但这此时却不用传递 socketfd 给到内核,因为内核通过 epoll_ctl 函数已经拿到了所要监控的 socketfd 列表

实际在你调用 epoll_create 后,内核就已经在开始准备帮你存储要监控的 socket 了,每次调用 epoll_ctl 只是在往内核的数据结构 > 红黑树,塞入新的 socketfd 罢了

Epoll 内核源码

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Set up listening socket, 'listen_sock' (socket(),
   bind(), listen()) */

epollfd = epoll_create(10);
if (epollfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_pwait");
        exit(EXIT_FAILURE);
    }

    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                            (struct sockaddr *) &local, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
	}
}

socket()、bind()、listen() 是所有的 I/O 模型都必须要经过的操作

  • epollfd=epoll_create(10):创建一个 epoll 文件描述符 > epfd,用于执行后续所有的 epoll 操作
  • 若 epoll_create 函数返回 -1,代表操作失败,失败的原因可能是内核中文件描述符的大小超出了限制
  • 通过 epoll_ctl 函数将新获取的 socket 套接字放入到 epoll 红黑树中,在内核会单独为 epoll 开辟一些数据结构,存放这些 socket 信息
  • EPOLL_CTL_ADD 代表新增 op 操作,成功返回 0,失败则返回 -1
  • 第一个死循环进行 wait 阻塞等待,主要是调用 epoll_wait 函数,类似 select/poll 的操作,从红黑树中复制出来的链表有状态的文件描述符,将拿到的结果存放在 events 数组中

epoll_wait(epollfd, events, MAX_EVENTS, -1):第四个参数的 -1 代表不超时

  • 当拿到有结果的 events 数组以后,对这些有状态的文件描述符进行遍历,若当前文件描述符等于传入的文件描述符,那么则对当前描述符进行 accept 函数调用,将套接字对应的 IP、Port 进行绑定,成功则返回文件描述符,否则返回 -1

此时,代表有新的客户端连接了,需要进行 accept 监听,生成一个新的 socketfd,并且设置为非阻塞运行的方式,调用 epoll_ctl 将新的 socketfd 放入到红黑树中

  • 若当前文件描述符不等于传入的文件描述符,那么就使用已被监听的文件描述符中的数据

通过命令:cat /proc/sys/fs/epoll/max_user_watches,可以查看系统上所有 epoll 实例注册的文件描述符总数的最大限制

在这里插入图片描述

图解分析

其实 Epoll 的模型大致上和 select/poll 模型大致上是一样的,只不过它们额外做的处理工作不一样而已,下面具体来介绍

epoll VS select/poll 工作原理

在这里插入图片描述

如上图,所有的 I/O 模型,都会经过三个函数的调用:socket -> bind -> listen,然后 accept 等待客户端建立连接再分配新的 fd 文件描述符!

经历过三个函数调用以后,epoll、select/poll 做以下对比:

  1. 经过通用的函数调用以后,在 Epoll 中会调用 epoll_create 函数生成 fd7,再调用 epoll_ctl 将应用程序与客户端之间新创建的文件描述符放入到内核中维护的红黑树数据结构中,当有客户端有数据 Send-Queue 发送到网卡,然后会到达对应文件描述符 fd4「socket 函数生成的文件描述符」Buffer 缓冲区中,在此时,epoll 对比 select/poll 多做了一下延伸处理工作:将红黑树里有状态的文件描述符 fds 拷贝到链表中,随即在调用 epoll_wait 函数就可以从链表中取出所有有状态(R/W)的 fds
  2. 经过通用的函数调用以后,在 select/poll 中会通过 socket 生成的文件描述符 fd 到内核循环遍历全量的文件描述符以后,返回哪些有状态(R/W)的 fds,当我们在应用程序什么时候调用 select 方法就会触发一次全量的 fds 遍历
  3. 无论是 epoll 还是 select/poll,即使不调用内核,内核也会随着中断的处理机制完成所有 fd 文件描述符的状态设置,而 Epoll 就是在中断处理时多做了一件事情:将红黑树里有状态的 fd 拷贝到了链表中,当应用程序要取来进行处理时,直接取链表里面的 fds 即可(O(1)),而不需要像 select/poll 那样去循环遍历(O(n)

epoll VS select/poll 日志追踪

可以通过 strace 命令来追踪 epoll、select/poll 内核底层的源码是如何处理的,Java 代码与上一篇讲解的 select/poll 多路复用是一致的。

select/poll

运行命令:

strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider SelectMultiplexingSocketThread

JVM native 分配了数组来保存 fd 信息,strace 源码追踪:

socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 4
# 如 Java 代码:server.configureBlocking(false)
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK)    = 0

bind(4, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::", &sin6_addr), ...) = 0

listen(4, 50)
# 返回 = 1,代表一个 fd 有事件到来
# 返回 = -1,代表非阻塞情况下,没有事件
# 返回 = 0,代表调用超时且没有事件返回
ppoll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, NULL, NULL, 0) = 1 ([{fd=4, revents=POLLIN}])
# 新的客户端连接进来,分配的是 fd7
accept(4, {sa_family=AF_INET6, sin6_port=htons(60292), inet_pton(AF_INET6, "::1", ...) = 7
fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0

epoll

strace -ff -o epoll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider SelectMultiplexingSocketThread

运行命令:

socket(AF_UNIX, SOCK_STREAM, 0)         = 4
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
bind(4, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::",...) = 0
listen(4, 50) 
# 创建了 epfd 7
epoll_create1(0)                        = 7
# 将 socket 返回的文件描述符放入红黑树中
epoll_ctl(7, EPOLL_CTL_ADD, 4, {EPOLLIN, {u32=4, u64=545460846596}}) = 0
# 未设置超时时间会一直阻塞,设置了超时时间,在时间内无事件会返回 0
epoll_pwait(7,
# 新的客户端连接进来,分配 fd8
accept(4, {sa_family=AF_INET6, sin6_port=htons(60294), inet_pton(AF_INET6, "::1", &sin6_addr), ...) = 8
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
# 将新客户端 fd8 天假到红黑树中
epoll_ctl(7, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=545460846600}}) = 0
# 继续循环 epoll_wait
epoll_pwait(7,

在 Java NIO 包下 Selector 通过一套代码在底层实现了 select/poll、epoll 两种 I/O 模型,对应的实现类分别是:sun.nio.ch.PollSelectorProvider、sun.nio.ch.EPollSelectorProvider

Epoll 优势之处

Epoll 高效在于:当我们调用 epoll_ctl 往内核塞入百万个 socket 时,epoll_wait 仍然可以飞快的返回,并会有效的将有发生事件的 socket 给到应用程序;这主要是在调用 epoll_create 时,内核除了在文件系统里建了 epfd,还在内核中建立了一个红黑树结构用于存储以后 epoll_ctl 传来的 socket 以外,还会再建立一个链表,用于存储哪些准备就绪的事件,当 epoll_wait 调用时,只需要仅仅观察这个链表有没有数据即可,有数据就返回,无数据就 sleep 等待 timeout 时间到,所以,epoll_wait 非常快.

每次都是 O(1) 的操作,不会在内核中发生循环遍历寻找的动作,以及也会减少用户态、内核态之间的大额数据交互,减少了资源的浪费及无效时间的行为.

总结

该篇博文主要介绍的就是比较重要比较核心的多路复用模型 Epoll,先简略说明 Epoll 重要的三大函数:epoll_create、epoll_ctl、epoll_wait,在其中说到了 Epoll 事件接口:边沿触发(ET:edge-triggered)、水平触发(LT:level-triggered),提及到了 Epoll 内核中关键的源码部分,使用三大函数巧妙结合起来实现 epoll 高效的多路复用,在底层采用红黑树结构存储所有的 socket fd 信息,采用链表结构存储所有有事件状态的 socket fd 信息,最后图解+日志追踪分析了 Epoll 与 select/poll 之间的区别以及介绍 Epoll 优势之处,希望您能够喜欢,感谢三连支持!

参考文献:

  1. 《UNIX网络编程 卷1:套接字联网API(第3版)》— [美] W. Richard Stevens Bill Fenner Andrew M. Rudoff
  2. Epoll原理 — 千寻
  3. Epoll — dreamgoing

学习帮助文档:

  • man pages:yum install man
  • pthread man pages:yum -y install man-pages

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【FPGA】综合设计练习题目

前言 这是作者这学期上的数电实验期末大作业的题目&#xff0c;综合性还是十分强的&#xff0c;根据组号作者是需要做“4、篮球比赛计分器”&#xff0c;相关代码会在之后一篇发出来&#xff0c;这篇文章用于记录练习题目&#xff0c;说不定以后有兴趣或者有时间了回来做做。 …

随机拆分文件夹划分训练验证集

import os from shutil import copy, rmtree import randomdef mk_file(file_path: str):if os

智能优化算法应用:基于鸟群算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于鸟群算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于鸟群算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.鸟群算法4.实验参数设定5.算法结果6.参考文献7.MA…

数据中心到底是如何工作的?

数字时代的数据中心&#xff0c;就如同网络世界的心脏&#xff0c;它的鼓动关系到整个网络生态的运转。但这个复杂而庞大的数据枢纽背后隐藏着怎样的精密机制&#xff0c;是许多人颇感好奇的谜。 数字时代的数据中心&#xff0c;就如同网络世界的心脏&#xff0c;它的鼓动关系…

【CSP】202303-1_田地丈量Python实现

文章目录 [toc]试题编号试题名称时间限制内存限制问题描述输入格式输出格式样例输入样例输出样例解释子任务Python实现 试题编号 202303-1 试题名称 田地丈量 时间限制 1.0s 内存限制 512.0MB 问题描述 西西艾弗岛上散落着 n n n块田地&#xff0c;每块田地可视为平面直角坐标…

UDP群聊

客户端 import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader…

PyTorch深度学习实战(25)——自编码器

PyTorch深度学习实战&#xff08;25&#xff09;——自编码器 0. 前言1. 自编码器2. 使用 PyTorch 实现自编码器小结系列链接 0. 前言 自编码器 (Autoencoder) 是一种无监督学习的神经网络模型&#xff0c;用于数据的特征提取和降维&#xff0c;它由一个编码器 (Encoder) 和一…

招不到人?用C语言采集系统批量采集简历

虽说现在大环境不太好&#xff0c;很多人面临着失业再就业风险&#xff0c;包括企业则面临着招人人&#xff0c;找对口专业难得问题。想要找到适合自己公司的人员&#xff0c;还要得通过爬虫获取筛选简历才能从茫茫人海中找到公司得力干将。废话不多说&#xff0c;直接开整。 1…

Github仓库远程操作——简单版

Github远程操作 github仓库简单的远程操作&#xff0c;更多复杂的功能请参考github官方文档 标题 Github远程操作添加公钥到githubGithub仓库远程操作 远程操作之前&#xff0c;先添加本地的公钥到github 添加公钥到github 创建本地ssh公私钥&#xff1a;使用powershell或者gi…

(1)(1.7) HOTT telemetry

文章目录 前言 1 布线和设置 2 参数说明 前言 Plane-4.0.0&#xff08;及更高版本&#xff09;、Copter-4.0.4&#xff08;及更高版本&#xff09;和 Rover-4.1.0&#xff08;及更高版本&#xff09;支持 Graupner HOTT 遥测技术。 1 布线和设置 与自动驾驶仪的连接可通过…

Jenkins项目部署CICD

目录 什么是CI/CD 常用 CI/CD 工具 主要步骤 1、点击新建任务 2、构建自由风格项目 3、填写内容 ①、General 1&#xff09;描述 2&#xff09;丢弃旧的构建 ②、源码管理 1&#xff09;Repository URL 2&#xff09;Credentials 3&#xff09;Branches to build…

破局:国内市场确实存在“消费升级”和“消费降级”,3.0全新新零售商业模式

国内市场确实存在“消费升级”和“消费降级”两个趋势&#xff0c;这是由于不同消费者群体的需求和购买力存在差异。消费升级主要发生在高端市场&#xff0c;消费者愿意为高品质、高价值、高价格的商品和服务付出更多。而消费降级则主要发生在中低端市场&#xff0c;消费者更加…

【教程】Ipa Guard为iOS应用提供免费加密混淆方案

概述&#xff1a;使用ios加固工具对ios代码保护&#xff0c;保护ios项目中的核心代码&#xff0c; #ipagurd年终大促百厂联动暖冬特惠&#xff0c;超多软控件立享惊喜优惠>> ​ 简介 iOS加固保护是直接针对ios ipa二进制文件的保护技术&#xff0c;可以对iOS APP中的可…

git根据commit id强制推送,撤销远程仓库代码

由于将把不用发版的需求合并上去了&#xff0c;现在想撤回&#xff0c;可以根据以下操作进行 注意撤回、强制推送有风险&#xff0c;记得强制撤回前&#xff0c;备份好代码 确保本地仓库中包含你想要推送的 commit&#xff1a; 这里你要经常使用命令进行操作的话&#xff0c;就…

maui下sqlite演示增删改查

数据操作类 有分页 todoitemDatabase.cs&#xff1a; using SQLite; using TodoSQLite.Models;namespace TodoSQLite.Data {public class TodoItemDatabase{SQLiteAsyncConnection Database;public TodoItemDatabase(){}// 初始化数据库连接和表async Task Init(){if (Databa…

Java:TCP 通信方法(基本发送 + 接收)并 实现文件传输且反馈

TCP 通信编程 TCP:是一种可靠的网络协议&#xff0c;再通信两端都建立一个Socket对象。 通信之前要保证连接已经建立。 通过Socket产生IO流进行通信。 创建对象时&#xff0c;会连接服务器&#xff0c;连接不上&#xff0c;会报错。 所以&#xff0c;先运行服务端&#xff0c;再…

Triton算法服务部署:初识与试用【Hello world】

0. 写在前面 Triton Inference Server 是一款开源推理服务软件&#xff0c;可简化 AI 推理。其可以部署来自多个深度学习和机器学习框架的任何 AI 模型&#xff0c;包括 TensorRT、TensorFlow、PyTorch、ONNX、OpenVINO、Python、RAPIDS FIL 等。Triton 支持在 NVIDIA GPU、x8…

【C++】哈希表

文章目录 哈希概念哈希冲突哈希函数哈希表闭散列开散列 开散列与闭散列比较 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站。 哈希概念 顺…

微服务项目部署

启动rabbitmq \RabbitMQ\rabbitmq_server-3.8.2\sbin 找到你的安装路径 找到\sbin路径下执行这些命令即可 rabbitmqctl status //查看当前状态 rabbitmq-plugins enable rabbitmq_management //开启Web插件 rabbitmq-server start //启动服务 rabbitmq-server stop //停止服务…

不需要联网的ocr项目

地址 GitHub - plantree/ocr-pwa: A simple PWA for OCR, based on Tesseract. 协议 mit 界面 推荐理由 可以离线使用&#xff0c;隐私安全