Zookeeper3.7源码分析
能力指标
- 能基于Maven导入最新版Zookeeper源码
- 能说出Zookeeper单机启动流程
- 了解Zookeeper默认通信中4个线程的作用
- 把握Zookeeper业务解决源码解决流程
- 可能在Zookeeper源码中Debug测试通信过程
1 Zookeeper源码导入
Zookeeper是一个高可用的分布式数据管理和协调框架,并且可能很好的保障分布式环境中数据的一致性。在越来越多的分布式系。在越来越多的分布式系统(Hadoop、HBase、Kafka)中,Zookeeper都作为外围组件应用。
咱们以后课程次要是钻研Zookeeper源码,须要将Zookeeper工程导入到IDEA中,老版的zk是通过ant进行编译的,但最新的zk(3.7)源码中曾经没了build.xml
,而多了pom.xml
,也就是说构建形式由原先的Ant变成了Maven,源码下下来后,间接编译、运行是跑不起来的,有一些配置须要调整。
1.1 工程导入
Zookeeper各个版本源码下载地址https://github.com/apache/zoo…,咱们能够在该仓库下抉择不同的版本,咱们抉择最新版本,以后最新版本为3.7,如下图:
找到我的项目下载地址,咱们抉择https
地址,并复制该地址,通过该地址把我的项目导入到IDEA
中。
点击IDEA的VSC>Checkout from Version Controller>GitHub
,操作如下图:
克隆我的项目到本地:
我的项目导入本地后,成果如下:
我的项目运行的时候,缺一个版本对象,创立org.apache.zookeeper.version.Info
,代码如下:
public interface Info {
public static final int MAJOR=3;
public static final int MINOR=4;
public static final int MICRO=6;
public static final String QUALIFIER=null;
public static final int REVISION=-1;
public static final String REVISION_HASH = "1";
public static final String BUILD_DATE="2020-12-03 09:29:06";
}
1.2 Zookeeper源码谬误解决
在zookeeper-server
中找到org.apache.zookeeper.server.quorum.QuorumPeerMain
并启动该类,启动前做如下配置:
启动的时候会会报很多谬误,比方缺包、缺对象,如下几幅图:
为了解决下面的谬误,咱们须要手动引入一些包,pom.xml
引入如下依赖:
<!--引入依赖-->
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
<version>1.1.7.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
</dependency>
1.3 Zookeeper命令(自学)
咱们要想学习Zookeeper,须要先学会应用Zookeeper,它有很多丰盛的命令,借助这些命令能够深刻了解Zookeeper,咱们启动源码中的客户端就能够应用Zookeeper相干命令。
启动客户端org.apache.zookeeper.ZooKeeperMain
,如下图:
启动后,日志如下:
1)节点列表:ls /
ls /
[dubbo, zookeeper]
ls /dubbo
[com.itheima.service.CarService]
2)查看节点状态:stat /dubbo
stat /dubbo
cZxid = 0x3
ctime = Thu Dec 03 09:19:29 CST 2020
mZxid = 0x3
mtime = Thu Dec 03 09:19:29 CST 2020
pZxid = 0x4
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 1
节点信息参数阐明如下:
key | value |
---|---|
cZxid = 0x3 |
创立节点时的事务ID |
ctime = Thu Dec 03 09:19:29 CST 2020 |
最初批改节点时的事务ID |
mZxid = 0x31 |
最初批改节点时的事务ID |
mtime = Sat Mar 16 15:38:34 CST 2019 |
最初批改节点时的工夫 |
pZxid = 0x31 |
示意该节点的子节点列表最初一次批改的事务ID,增加子节点或删除子节点就会影响子节点列表,然而批改子节点的数据内容则不影响该ID(留神,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid) |
cversion = 0 |
子节点版本号,子节点每次批改版本号加1 |
dataVersion = 0 |
数据版本号,数据每次批改该版本号加1 |
aclVersion = 0 |
权限版本号,权限每次批改该版本号加1 |
ephemeralOwner = 0x0 |
创立该长期节点的会话的sessionID。(如果该节点是长久节点,那么这个属性值为0) |
dataLength = 22 |
该节点的数据长度 |
numChildren = 0 |
该节点领有子节点的数量(只统计间接子节点的数量) |
3)创立节点:create /dubbo/code java
create /dubbo/code java
Created /dubbo/code
其中code示意节点,java示意节点下的内容。
4)查看节点数据:get /dubbo/code
get /dubbo/code
java
5)删除节点:delete /dubbo/code
|| deleteall /dubbo/code
删除没有子节点的节点:delete /dubbo/code
删除所有子节点:deleteall /dubbo/code
6)历史操作命令:history
history
1 - ls /dubbo
2 - ls /dubbo/code
3 - get /dubbo/code
4 - get /dubbo/code
5 - create /dubbo/code java
6 - get /dubbo/code
7 - get /dubbo/code
8 - delete /dubbo/code
9 - get /dubbo/code
10 - listquota path
11 - history
1.4 Zookeeper剖析工具
Zookeeper装置比拟不便,在装置一个集群当前,查看数据却比拟麻烦,上面介绍Zookeeper的数据查看工具——ZooInspector。
下载地址:https://issues.apache.org/jir…
下载压缩包后,解压后,咱们须要运行zookeeper-dev-ZooInspector.jar
:
输出账号密码,就能够连贯Zookeeper了,如下图:
连贯后,Zookeeper信息如下:
节点操作:减少节点、批改节点、删除节点
1.5 Zookeeper案例利用
咱们将材料中工程\dubbo
工程导入到IDEA中,上图是他们的调用关系,那么问题来了:
- 生产者向Zookeeper注册服务信息,Zookeeper把数据存哪儿了?
- 集群环境下,如果某个节点数据变更了,Zookeeper如何监听到的?
- 集群环境下各个节点的数据如何同步?
- 如果某个节点挂了,Zookeeper如何选举呢?
- ……..
带着下面的疑难,咱们开始钻研Zookeeper源码。
2 ZK服务启动流程源码分析
ZooKeeper
能够以standalone
、分布式的形式部署,standalone
模式下只有一台机器作为服务器,ZooKeeper
会丢失高可用个性,分布式是应用多个机器,每台机器上部署一个ZooKeeper
服务器,即便有服务器宕机,只有少于半数,ZooKeeper
集群仍然能够失常对外提供服务,集群状态下Zookeeper
是具备高可用个性。
咱们接下来对ZooKeeper
以standalone
模式启动以及集群模式做一下源码剖析。
2.1 ZK单机/集群启动流程
如上图,上图是Zookeeper
单机/集群启动流程,每个细节所做的事件都在上图有阐明,咱们接下来依照流程图对源码进行剖析。
2.2 ZK启动入口剖析
启动入口类:QuorumPeerMain
该类是zookeeper
单机/集群的启动入口类,是用来加载配置、启动QuorumPeer
(选举相干)线程、创立ServerCnxnFactory
等,咱们能够把代码切换到该类的主办法(main
)中,从该类的主办法开始剖析,main
办法代码剖析如下:
下面main办法尽管只是做了初始化配置,但调用了initializeAndRun()
办法,initializeAndRun()
办法中会依据配置来决定启动单机Zookeeper还是集群Zookeeper,源码如下:
如果启动单机版,会调用ZooKeeperServerMain.main(args);
,如果启动集群版,会调用QuorumPeerMain.runFromConfig(config);
,咱们接下来对单机版启动做源码具体分析,集群版在前面章节中解说选举机制时具体解说。
2.3 ZK单机启动源码分析
针对ZK单机启动源码办法调用链,咱们曾经提前做了一个办法调用关系图,咱们解说ZK单机启动源码,将和该图进行一一匹对,如下图:
1)单机启动入口
依照下面的源码剖析,咱们找到ZooKeeperServerMain.main(args)
办法,该办法调用了ZooKeeperServerMain
的initializeAndRun
办法,在initializeAndRun
办法中执行初始化操作,并运行Zookeeper服务,main办法如下:
2)配置文件解析
initializeAndRun()
办法会注册JMX,同时解析zoo.cfg
配置文件,并调用runFromConfig()
办法启动Zookeeper服务,源码如下:
3)单机启动主流程
runFromConfig
办法是单机版启动的次要办法,该办法会做如下几件事:
1:初始化各类运行指标,比方一次提交数据最大破费多长时间、批量同步数据大小等。
2:初始化权限操作,例如IP权限、Digest权限。
3:创立事务日志操作对象,Zookeeper中每次减少节点、批改数据、删除数据都是一次事务操作,都会记录日志。
4:定义Jvm监控变量和常量,例如正告工夫、告警阀值次数、提醒阀值次数等。
5:创立ZookeeperServer,这里只是创立,并不在ZooKeeperServerMain类中启动。
6:启动Zookeeper的控制台治理对象AdminServer,该对象采纳Jetty启动。
7:创立ServerCnxnFactory,该对象其实是Zookeeper网络通信对象,默认应用了NIOServerCnxnFactory。
8:在ServerCnxnFactory中启动ZookeeperServer服务。
9:创立并启动ContainerManager,该对象通过Timer定时执行,清理过期的容器节点和TTL节点,执行周期为分钟。
10:避免主线程完结,阻塞主线程。
办法源码如下:
4)网络通信对象创立
下面办法在创立网络通信对象的时候调用了ServerCnxnFactory.createFactory()
,该办法其实是依据系统配置创立Zookeeper通信组件,可选的有NIOServerCnxnFactory(默认)
和NettyServerCnxnFactory
,对于通信对象咱们会在前面进行具体解说,该办法源码如下:
5)单机启动
cnxnFactory.startup(zkServer);
办法其实就是启动了ZookeeperServer
,它调用NIOServerCnxnFactory
的startup
办法,该办法中会调用ZookeeperServer
的startup
办法启动服务,ZooKeeperServerMain
运行到shutdownLatch.await();
主线程会阻塞住,源码如下:
启动后,日志如下:
3 ZK网络通信源码分析
Zookeeper
作为一个服务器,天然要与客户端进行网络通信,如何高效的与客户端进行通信,让网络IO
不成为ZooKeeper
的瓶颈是ZooKeeper
急需解决的问题,ZooKeeper
中应用ServerCnxnFactory
治理与客户端的连贯,其有两个实现,一个是NIOServerCnxnFactory
,应用Java原生NIO
实现;一个是NettyServerCnxnFactory
,应用netty实现;应用ServerCnxn
代表一个客户端与服务端的连贯。
从单机版启动中能够发现Zookeeper
默认通信组件为NIOServerCnxnFactory
,他们和ServerCnxnFactory
的关系如下图:
3.1 NIOServerCnxnFactory工作流程
个别应用Java NIO的思路为应用1个线程组监听OP_ACCEPT
事件,负责解决客户端的连贯;应用1个线程组监听客户端连贯的OP_READ
和OP_WRITE
事件,解决IO事件(netty也是这种实现形式).
但ZooKeeper并不是如此划分线程性能的,NIOServerCnxnFactory
启动时会启动四类线程:
1:accept thread:该线程接管来自客户端的连贯,并将其调配给selector thread(启动一个线程)。
2:selector thread:该线程执行select(),因为在解决大量连贯时,select()会成为性能瓶颈,因而启动多个selector thread,应用零碎属性zookeeper.nio.numSelectorThreads配置该类线程数,默认个数为 外围数/2。
3:worker thread:该线程执行根本的套接字读写,应用零碎属性zookeeper.nio.numWorkerThreads配置该类线程数,默认为外围数∗2外围数∗2.如果该类线程数为0,则另外启动一线程进行IO解决,见下文worker thread介绍。
4:connection expiration thread:若连贯上的session已过期,则敞开该连贯。
这四个线程在NIOServerCnxnFactory
类上有阐明,如下图:
ZooKeeper
中对线程须要解决的工作做了更细的拆分,解决了有大量客户端连贯的状况下,selector.select()
会成为性能瓶颈,将selector.select()
拆分进去,交由selector thread
解决。
3.2 NIOServerCnxnFactory源码
NIOServerCnxnFactory的源码剖析咱们将依照下面所介绍的4个线程实现相干剖析,并实现数据操作,在程序中获取指定数据。
3.2.1 AcceptThread分析
为了让大家更容易了解AcceptThread,咱们把它的构造和办法调用关系画了一个具体的流程图,如下图:
在NIOServerCnxnFactory
类中有一个AccpetThread
线程,为什么说它是一个线程?咱们看下它的继承关系:AcceptThread > AbstractSelectThread > ZooKeeperThread > Thread
,该线程接管来自客户端的连贯,并将其调配给selector thread(启动一个线程)。
该线程执行流程:run
执行selector.select()
,并调用doAccept()
接管客户端连贯,因而咱们能够着重关注doAccept()
办法,该类源码如下:
doAccept()
办法用于解决客户端链接,当客户端链接Zookeeper
的时候,首先会调用该办法,调用该办法执行过程如下:
1:和以后服务建设链接。
2:获取近程客户端计算机地址信息。
3:判断以后链接是否超出最大限度。
4:调整为非阻塞模式。
5:轮询获取一个SelectorThread,将以后链接调配给该SelectorThread。
6:将以后申请增加到该SelectorThread的acceptedQueue中,并唤醒该SelectorThread。
doAccept()
办法源码如下:
下面代码中addAcceptedConnection
办法如下:
咱们把我的项目中的分布式案例服务启动,能够看到如下日志打印:
AcceptThread----------链接服务的IP:127.0.0.1
3.2.2 SelectorThread分析
同样为了更容易梳理SelectorThread
,咱们也把它的构造和办法调用关系梳理成了流程图,如下图:
该线程的次要作用是从Socket读取数据,并封装成workRequest
,并将workRequest
交给workerPool
工作线程池解决,同时将acceptedQueue中未解决的链接取出,并未每个链接绑定OP_READ
读事件,并封装对应的上下文对象NIOServerCnxn
。SelectorThread
的run办法如下:
run()
办法中会调用select()
,而select()
中的外围调用中央是handleIO()
,咱们看名字其实就晓得这里是解决客户端申请的数据,但客户端申请数据并非在SelectorThread
线程中解决,咱们接着看handleIO()
办法。
handleIO()
办法会封装以后SelectorThread
为IOWorkRequest
,并将IOWorkRequest
交给workerPool
来调度,而workerPool
调度才是读数据的开始,源码如下:
3.2.3 WorkerThread分析
WorkerThread相比下面的线程而言,调用关系颇为简单,设计到了多个对象办法调用,次要用于解决IO,但并未对数据做出解决,数据处理将有业务链对象RequestProcessor解决,调用关系图如下:
ZooKeeper
中通过WorkerService
治理一组worker thread
线程,后面咱们在看SelectorThread
的时候,可能看到workerPool
的schedule办法被执行,如下图:
咱们跟踪workerPool.schedule(workRequest);
能够发现它调用了WorkerService.schedule(workRequest) > WorkerService.schedule(WorkRequest, long)
,该办法创立了一个新的线程ScheduledWorkRequest
,并启动了该线程,源码如下:
ScheduledWorkRequest
实现了Runnable
接口,并在run()
办法中调用了IOWorkRequest
中的doWork
办法,在该办法中会调用doIO
执行IO数据处理,源码如下:
IOWorkRequest
的doWork
源码如下:
接下来的调用链路比较复杂,咱们把外围步骤列出,在能间接看到数据读取的中央详细分析源码。下面办法调用链路:NIOServerCnxn.doIO()>readPayload()>readRequest() >ZookeeperServer.processPacket()
,最初一步办法是获取外围数据的中央,咱们能够批改下代码读取数据:
增加测试代码如下:
//==========测试 Start===========
//定义接管输出流对象(输入流)
ByteArrayOutputStream os = new ByteArrayOutputStream();
//将网络输出流读取到输入流中
byte[] buffer = new byte[1024];
int len=0;
while ((len=bais.read(buffer))!=-1){
os.write(buffer,0,len);
}
String result = new String(os.toByteArray(),"UTF-8");
System.out.println("processPacket---------------读到的数据:"+result);
//==========测试 End===========
咱们启动客户端创立一个demo节点,并增加数据为 abcdefg
create /demo abcdefg
控制台数据如下:
测试实现后,不要忘了将该测试正文掉。咱们能够执行其余增删改查操作,能够输入RequestHeader.type
查看操作类型,操作类型代码在ZooDefs
中有标识,罕用的操作类型如下:
int create = 1;
int delete = 2;
int exists = 3;
int getData = 4;
int setData = 5;
int getACL = 6;
int setACL = 7;
int getChildren = 8;
int sync = 9;
int ping = 11;
2.3.4 ConnectionExpirerThread分析
后盾启动ConnectionExpirerThread
清理线程清理过期的session
,线程中有限循环,执行工作如下:
2.3 ZK通信优劣总结
Zookeeper在通信方面默认应用了NIO,并反对扩大Netty实现网络数据传输。相比传统IO,NIO在网络数据传输方面有很多显著劣势:
1:传统IO在解决数据传输申请时,针对每个传输申请生成一个线程,如果IO异样,那么线程阻塞,在IO复原后唤醒解决线程。在同时解决大量连贯时,会实例化大量的线程对象。每个线程的实例化和回收都须要耗费资源,jvm须要为其调配TLAB,而后初始化TLAB,最初绑定线程,线程完结时又须要回收TLAB,这些都须要CPU资源。
2:NIO应用selector来轮询IO流,外部应用poll或者epoll,以事件驱动模式来相应IO事件的解决。同一时间只需实例化很少的线程对象,通过对线程的复用来进步CPU资源的应用效率。
3:CPU轮流为每个线程调配工夫片的模式,间接的实现单物理核解决多线程。当线程越多时,每个线程调配到的工夫片越短,或者循环调配的周期越长,CPU很多工夫都消耗在了线程的切换上。线程切换蕴含线程上个线程数据的同步(TLAB同步),同步变量同步至主存,下个线程数据的加载等等,他们都是很消耗CPU资源的。
4:在同时解决大量连贯,但沉闷连贯不多时,NIO的事件响应模式相比于传统IO有着极大的性能晋升。NIO还提供了FileChannel,以zero-copy的模式传输数据,相较于传统的IO,数据不须要拷贝至用户空间,可间接由物理硬件(磁盘等)通过内核缓冲区后间接传递至网关,极大的进步了性能。
5:NIO提供了MappedByteBuffer,其将文件间接映射到内存(这里的内存指的是虚拟内存,并不是物理内存),能极大的进步IO吞吐能力。
ZK在应用NIO通信尽管大幅晋升了数据传输能力,但也存在一些代码诟病问题:
1:Zookeeper通信源码局部学习老本高,须要把握NIO和多线程
2:多线程应用频率高,耗费资源多,但性能失去晋升
3:Zookeeper数据处理调用链路简单,多处存在外部类,代码构造不清晰,写法比拟经典
4 RequestProcessor解决申请源码分析
zookeeper
的业务解决流程就像工作流一样,其实就是一个单链表;在zookeeper
启动的时候,会确立各个节点的角色个性,即leader
、follower
和observer
,每个角色确立后,就会初始化它的工作责任链;
4.1 RequestProcessor构造
客户端申请过去,每次执行不同事务操作的时候,Zookeeper也提供了一套业务解决流程RequestProcessor
,RequestProcessor
的解决流程如下图:
咱们来看一下RequestProcessor
初始化流程,ZooKeeperServer.setupRequestProcessors()
办法源码如下:
它的创立步骤:
1:创立finalProcessor。
2:创立syncProcessor,并将finalProcessor作为它的下一个业务链。
3:启动syncProcessor。
4:创立firstProcessor(PrepRequestProcessor),将syncProcessor作为firstProcessor的下一个业务链。
5:启动firstProcessor。
syncProcessor
创立时,将finalProcessor
作为参数传递进来源码如下:
firstProcessor
创立时,将syncProcessor
作为参数传递进来源码如下:
PrepRequestProcessor/SyncRequestProcessor
关系图:
PrepRequestProcessor
和SyncRequestProcessor
的构造一样,都是实现了Thread
的一个线程,所以在这里初始化时便启动了这两个线程。
4.2 PrepRequestProcessor分析
PrepRequestProcessor
是申请处理器的第1个处理器,咱们把之前的申请业务解决衔接起来,一步一步剖析。ZooKeeperServer.processPacket()>submitRequest()>enqueueRequest()>RequestThrottler.submitRequest()
,咱们来看下RequestThrottler.submitRequest()
源码,它将以后申请增加到submittedRequests
队列中了,源码如下:
而RequestThrottler
继承了 ZooKeeperCriticalThread > ZooKeeperThread > Thread
,也就是说以后RequestThrottler
是个线程,咱们看看它的run
办法做了什么事,源码如下:
RequestThrottler
调用了ZooKeeperServer.submitRequestNow()
办法,而该办法又调用了firstProcessor
的办法,源码如下:
ZooKeeperServer.submitRequestNow()
办法调用了firstProcessor.processRequest()
办法,而这里的firstProcessor
就是初始化业务解决链中的PrepRequestProcessor
,也就是说三个RequestProecessor
中最先调用的是PrepRequestProcessor
。
PrepRequestProcessor.processRequest()
办法将以后申请增加到了队列submittedRequests
中,源码如下:
下面办法中并未从submittedRequests
队列中获取申请,如何执行申请的呢,因为PrepRequestProcessor
是一个线程,因而会在run
中执行,咱们查看run
办法源码的时候发现它调用了pRequest()
办法,pRequest()
办法源码如下:
首先先执行pRequestHelper()
办法,该办法是PrepRequestProcessor
解决外围业务流程,次要是一些过滤操作,操作实现后,会将申请交给下一个业务链,也就是SyncRequestProcessor.processRequest()
办法解决申请。
咱们来看一下PrepRequestProcessor.pRequestHelper()
办法做了哪些事,源码如下:
从下面源码能够看出PrepRequestProcessor.pRequestHelper()
办法判断了客户端操作类型,但无论哪种操作类型简直都调用了pRequest2Txn()
办法,咱们来看看源码:
从下面代码能够看出pRequest2Txn()
办法次要做了权限校验、快照记录、事务信息记录相干的事,还并未波及数据处理,也就是说PrepRequestProcessor
其实是做了操作前权限校验、快照记录、事务信息记录相干的事。
咱们DEBUG调试一次,看看业务解决流程是否和咱们下面所剖析的统一。
增加节点:
create /zkdemo itheima
DEBUG测试如下:
客户端申请先通过ZooKeeperServer.submitRequestNow()
办法,并调用firstProcessor.processRequest()
办法,而firstProcessor
=PrepRequestProcessor
,如下图:
进入PrepRequestProcessor.pRequest()
办法,执行完pRequestHelper()
办法后,开始执行下一个业务链的办法,而下一个业务链nextProcessor
=SyncRequestProcessor
,如下测试图:
4.3 SyncRequestProcessor分析
剖析了PrepRequestProcessor
处理器后,接着来剖析SyncRequestProcessor
,该处理器次要是将申请数据高效率存入磁盘,并且申请在写入磁盘之前是不会被转发到下个处理器的。
咱们先看申请被增加到队列的办法:
同样SyncRequestProcessor
是一个线程,执行队列中的申请也在线程中触发,咱们看它的run办法,源码如下:
run
办法会从queuedRequests
队列中获取一个申请,如果获取不到就会阻塞期待直到获取到一个申请对象,程序才会持续往下执行,接下来会调用Snapshot Thread
线程实现将客户端发送的数据以快照的形式写入磁盘,最终调用flush()
办法实现数据提交,flush()
办法源码如下:
flush()
办法实现了数据提交,并且会将申请交给下一个业务链,下一个业务链为FinalRequestProcessor
。
4.4 FinalRequestProcessor分析
后面剖析了SyncReqeustProcessor
,接着剖析申请解决链中最初的一个处理器FinalRequestProcessor
,该业务解决对象次要用于返回Response。
4.5 ZK业务链解决优劣总结
Zookeeper业务链解决,思维遵循了AOP思维,但并未采纳相干技术,为了晋升效率,依然大幅应用到了多线程。正因为有了业务链路解决先后顺序,使得Zookeeper业务解决流程更清晰更容易了解,但大量混入了多线程,也似的学习成本增加。
本文由传智教育博学谷 – 狂野架构师教研团队公布,转载请注明出处!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源
发表回复