当前位置: 首页 > news >正文

分布式架构

一、基础概念

分布式处理是将不同地点的,或具有不同功能的,或拥有不同数据的多台计算机通过通信网络连接起来,在控制系统的统一管理控制下,协调地完成大规模信息处理任务的计算机系统。

1 分布式性能指标

1.QPS每秒查询率(Query Per Second)

QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够接受相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
qps越高,说明每秒查询的数量越高,当一个系统的qps过高时,可以考虑系统是否能承受这么高的qps.

2.TPS:是TransactionsPerSecond的缩写,也就是事务数/秒。
它是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

tps越高,说明每秒处理的事务数更高,系统处理能力越强,系统性能也就越好。

3.吞吐量:吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量(以比特、字节、分组等测量)。

我自己的理解为单位时间系统所承受的请求到响应的数目。

吞吐量越高,系统性能越好。

1.系统吞吐量、TPS(QPS)、用户并发量、性能测试概念和公式
https://my.oschina.net/wugong/blog/1633176

思考

1.机器数量选型

按二八定律来看,如果每天 80% 的访问集中在 20% 的时间里,这 20% 时间就叫做峰值时间。

  • 公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)
  • 机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器

1、每天300w PV 的在单台机器上,这台机器需要多少QPS?
( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)

2、如果一台机器的QPS是58,需要几台机器来支持?
139 / 58 = 3

2.最佳线程数选型

单线程QPS公式:QPS=1000ms/RT

对同一个系统而言,支持的线程数越多,QPS越高。

假设一个RT是80ms,则可以很容易的计算出QPS,QPS = 1000/80 = 12.5

多线程场景,如果把服务端的线程数提升到2,那么整个系统的QPS则为 2*(1000/80) = 25
可见QPS随着线程的增加而线性增长,那QPS上不去就加线程呗,听起来很有道理,公司也说的通,但是往往现实并非如此。

3.高并发QPS的定义

首先是无状态前端机器不足以承载请求流量,需要进行水平扩展,一般QPS是千级。
然后是关系型数据库无法承载读取或写入峰值,需要数据库横向扩展或引入nosql,一般是千到万级。
之后是单机nosql无法承载,需要nosql横向扩展,一般是十万到百万QPS。
最后是难以单纯横向扩展nosql,比如微博就引入多级缓存架构,这种架构一般可以应对百万到千万对nosql的访问QPS。
当然面向用户的接口请求一般到不了这个量级,QPS递增大多是由于读放大造成的压力,但也属于高并发架构考虑的范畴。

相关联的领域概念

1.微服务
参考我写的微服务文章:
微服务架构
https://blog.csdn.net/sinat_34814635/article/details/79931252

2.线程数选型

跟I/O模型紧密相关的是线程池,线程池的调优就是设置合理的线程池参数。

这里面最核心的就是如何确定maxThreads的值,如果这个参数设置小了,Tomcat会发生线程饥饿,并且请 求的处理会在队列中排队等待,导致响应时间变长;

如果maxThreads参数值过大,同样也会有问题,因为 服务器的CPU的核数有限,线程数太多会导致线程在CPU上来回切换,耗费大量的切换开销。

那maxThreads设置成多少才算是合适呢?为了理解清楚这个问题,我们先来看看什么是利特尔法则 (Little’s Law)。

系统中的请求数 = 请求的到达速率 × 每个请求处理时间

其实这个公式很好理解,我举个我们身边的例子:我们去超市购物结账需要排队,但是你是如何估算一个队 列有多长呢?队列中如果每个人都买很多东西,那么结账的时间就越长,队列也会越长;同理,短时间一下 有很多人来收银台结账,队列也会变长。因此队列的长度等于新人加入队列的频率乘以平均每个人处理的时 间。

计算出了队列的长度,那么我们就创建相应数量的线程来处理请求,这样既能以最快的速度处理完所有请 求,同时又没有额外的线程资源闲置和浪费。

假设一个单核服务器在接收请求:

  • 如果每秒10个请求到达,平均处理一个请求需要1秒,那么服务器任何时候都有10个请求在处理,即需要 10个线程
  • 如果每秒10个请求到达,平均处理一个请求需要2秒,那么服务器在每个时刻都有20个请求在处理,因此 需要20个线程
  • 如果每秒10000个请求到达,平均处理一个请求需要1秒,那么服务器在每个时刻都有10000个请求在处 理,因此需要10000个线程。

因此可以总结出一个公式:

线程池大小 = 每秒请求数 × 平均请求处理时间

这是理想的情况,也就是说线程一直在忙着干活,没有被阻塞在I/O等待上。实际上任务在执行中,线程不 可避免会发生阻塞,比如阻塞在I/O等待上,等待数据库或者下游服务的数据返回,虽然通过非阻塞I/O模型 可以减少线程的等待,但是数据在用户空间和内核空间拷贝过程中,线程还是阻塞的。线程一阻塞就会让出 CPU,线程闲置下来,就好像工作人员不可能24小时不间断地处理客户的请求,解决办法就是增加工作人员 的数量,一个人去休息另一个人再顶上。对应到线程池就是增加线程数量,因此I/O密集型应用需要设置更 多的线程。

至此我们又得到一个线程池个数的计算公式,假设服务器是单核的:

线程池大小 = (线程I/O阻塞时间 + 线程CPU时间 )/ 线程CPU时间

其中:线程I/O阻塞时间 + 线程CPU时间 = 平均请求处理时间

对比一下两个公式,你会发现,平均请求处理时间在两个公式里都出现了,这说明请求时间越长,需要更多 的线程是毫无疑问的。 

不同的是第一个公式是用每秒请求数来乘以请求处理时间;而第二个公式用请求处理时间来除以线程CPU时 间,请注意CPU时间是小于请求处理时间的。 虽然这两个公式是从不同的角度来看待问题的,但都是理想情况,都有一定的前提条件。

1. 请求处理时间越长,需要的线程数越多,但前提是CPU核数要足够,如果一个CPU来支撑10000 TPS并 发,创建10000个线程,显然不合理,会造成大量线程上下文切换。

2. 请求处理过程中,I/O等待时间越长,需要的线程数越多,前提是CUP时间和I/O时间的比率要计算的足够 准确。

3. 请求进来的速率越快,需要的线程数越多,前提是CPU核数也要跟上。

那么在实际情况下,线程池的个数如何确定呢?这是一个迭代的过程,先用上面两个公式大概算出理想的线 程数,再反复压测调整,从而达到最优。

一般来说,如果系统的TPS要求足够大,用第一个公式算出来的线程数往往会比公式二算出来的要大。我建 议选取这两个值中间更靠近公式二的值。也就是先设置一个较小的线程数,然后进行压测,当达到系统极限 时(错误数增加,或者响应时间大幅增加),再逐步加大线程数,当增加到某个值,再增加线程数也无济于 事,甚至TPS反而下降,那这个值可以认为是最佳线程数。

线程池中其他的参数,最好就用默认值,能不改就不改,除非在压测的过程发现了瓶颈。如果发现了问题就 需要调整,比如maxQueueSize,如果大量任务来不及处理都堆积在maxQueueSize中,会导致内存耗尽, 这个时候就需要给maxQueueSize设一个限制。当然,这是一个比较极端的情况了。 再比如minSpareThreads参数,默认是25个线程,如果你发现系统在闲的时候用不到25个线程,就可以调 小一点;如果系统在大部分时间都比较忙,线程池中的线程总是远远多于25个,这个时候你就可以把这个 参数调大一点,因为这样线程池就不需要反复地创建和销毁线程了

1.3 技术选型

1.方向代理:nginx
2.网关
3.rpc服务器内部调用:dubbo
4.消息队列mq:rabbitmq
5.存储:mysql,es,redis,ck
6.服务监控:cat,
7.分布式链路追踪:Pingpoint

二、核心理论

1. 同步和异步通讯选型

1.同步通讯

同步通讯就像打电话,需要实时响应,而异步通讯就像发邮件,不需要马上回复。各有千秋,我们很难说谁比谁好。但是在面对超高吐吞量的场景下,异步处理就比同步处理有比较大的优势了,这就好像一个人不可能同时接打很多电话,但是他可以同时接收很多的电子邮件一样。

同步调用虽然让系统间只耦合于接口,而且实时性也会比异步调用要高,但是我们也需要知道同步调用会带来如下几个问题。

  • 同步调用需要被调用方的吞吐不低于调用方的吞吐。否则会导致被调用方因为性能不足而拖死调用方。换句话说,整个同步调用链的性能会由最慢的那个服务所决定。
  • 同步调用会导致调用方一直在等待被调用方完成,如果一层接一层地同步调用下去,所有的参与方会有相同的等待时间。这会非常消耗调用方的资源。因为调用方需要保存现场(Context)等待远端返回,所以对于并发比较高的场景来说,这样的等待可能会极度消耗资源。
  • 同步调用只能是一对一的,很难做到一对多。
  • 同步调用最不好的是,如果被调用方有问题,那么其调用方就会跟着出问题,于是会出现多米诺骨牌效应,故障一下就蔓延开来。

2.异步通讯

异步通讯相对于同步通讯来说,除了可以增加系统的吞吐量之外,最大的一个好处是其可以让服务间的解耦更为彻底,系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力。

异步通讯的三种方式

从实现复杂度分析,一下三种由易到难。
1.请求响应式

在这种情况下,发送方(sender)会直接请求接收方(receiver),被请求方接收到请求后,直接返回——收到请求,正在处理。

对于返回结果,有两种方法,一种是发送方时不时地去轮询一下,问一下干没干完。
另一种方式是发送方注册一个回调方法,也就是接收方处理完后回调请求方。这种架构模型在以前的网上支付中比较常见,页面先从商家跳转到支付宝或银行,商家会把回调的 URL 传给支付页面,支付完后,再跳转回商家的 URL。

这种方式比较适合和外部公司的一种异步通讯方式,因为与外部公司的对决不太可能用mq,因为成本太大了。

很明显,这种情况下还是有一定耦合的。是发送方依赖于接收方,并且要把自己的回调发送给接收方,处理完后回调。

2.通过订阅的方式

这种情况下,接收方(receiver)会来订阅发送方(sender)的消息,发送方会把相关的消息或数据放到接收方所订阅的队列中,而接收方会从队列中获取数据。

这种方式下,发送方并不关心订阅方的处理结果,它只是告诉订阅方有事要干,收完消息后给个 ACK 就好了,你干成啥样我不关心。这个方式常用于像 MVC(Model-View-Control)这样的设计模式下

这就好像下订单的时候,一旦用户支付完成了,就需要把这个事件通知给订单处理以及物流,订单处理变更状态,物流服务需要从仓库服务分配相应的库存并准备配送,后续这些处理的结果无需告诉支付服务。

为什么要做成这样?好了,重点来了!前面那种请求响应的方式就像函数调用一样,这种方式有数据有状态的往来(也就是说需要有请求数据、返回数据,服务里面还可能需要保存调用的状态),所以服务是有状态的。如果我们把服务的状态给去掉(通过第三方的状态服务来保证),那么服务间的依赖就只有事件了。

你知道,分布式系统的服务设计是需要向无状态服务(Stateless)努力的,这其中有太多的好处,无状态意味着你可以非常方便地运维。所以,事件通讯成为了异步通讯中最重要的一个设计模式。

就上面支付的那个例子,商家这边只需要订阅一个支付完成的事件,这个事件带一个订单号,而不需要让支付方知道自己的回调 URL,这样的异步是不是更干净一些?

但是,在这种方式下,接收方需要向发送方订阅事件,所以是接收方依赖于发送方。这种方式还是有一定的耦合。

这种方式一般适用于进程内的解耦合,进程间一般通过mq的方式。

3.通过 Broker 的方式

所谓 Broker,就是一个中间人,发送方(sender)和接收方(receiver)都互相看不到对方,它们看得到的是一个 Broker,发送方向 Broker 发送消息,接收方向 Broker 订阅消息。如下图所示。

在这里插入图片描述

这是完全的解耦。所有的服务都不需要相互依赖,而是依赖于一个中间件 Broker。这个 Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。

在 Broker 这种模式下,发送方的服务和接收方的服务最大程度地解耦。但是所有人都依赖于一个总线,所以这个总线就需要有如下的特性:

  • 必须是高可用的,因为它成了整个系统的关键;
  • 必须是高性能而且是可以水平扩展的;
  • 必须是可以持久化不丢数据的。

要做到这三条还是比较难的。当然,好在现在开源软件或云平台上 Broker 的软件是非常成熟的,所以节省了我们很多的精力。

3.异步通讯的设计重点

首先,我们需要知道,为什么要异步通讯。

  • 异步通讯最重要的是解耦服务间的依赖。最佳解耦的方式是通过 Broker 的机制。
  • 解耦的目的是让各个服务的隔离性更好,这样不会出现“一倒倒一片”的故障。
  • 异步通讯的架构可以获得更大的吞吐量,而且各个服务间的性能不受干扰相对独立。
  • 利用 Broker 或队列的方式还可以达到把抖动的吞吐量变成均匀的吞吐量,这就是所谓的“削峰”,这对后端系统是个不错的保护。
  • 服务相对独立,在部署、扩容和运维上都可以做到独立不受其他服务的干扰。

但我们需要知道这样的方式带来的问题,所以在设计成异步通信的时候需要注意如下事宜。

  • 用于异步通讯的中间件 Broker成为了关键,需要设计成高可用不丢消息的。另外,因为是分布式的,所以可能很难保证消息的顺序,因此你的设计最好不依赖于消息的顺序。
  • 异步通讯会导致业务处理流程不那么直观,因为像接力一样,所以在 Broker 上需要有相关的服务消息跟踪机制,否则出现问题后不容易调试。
  • 因为服务间只通过消息交互,所以业务状态最好由一个总控方来管理,这个总控方维护一个业务流程的状态变迁逻辑,以便系统发生故障后知道业务处理到了哪一步,从而可以在故障清除后继续处理。这样的设计常见于银行的对账程序,银行系统会有大量的外部系统通讯,比如跨行的交易、跨企业的交易,等等。所以,为了保证整体数据的一致性,或是避免漏处理及处理错的交易,需要有对账系统,这其实就是那个总控,这也是为什么银行有的交易是 T+1(隔天结算),就是因为要对个账,确保数据是对的。
  • 消息传递中,可能有的业务逻辑会有像 TCP 协议那样的 send 和 ACK 机制。比如:A 服务发出一个消息之后,开始等待处理方的
    ACK,如果等不到的话,就需要做重传。此时,需要处理方有幂等的处理,即同一件消息无论收到多少次都只处理一次。

2.3 幂等性

1. 接口幂等性

简而言之,就是同样的请求对接口发起的一次调用和多次调用,所产生的结果都是一致的。
下面是一些典型场景:

  • 订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
  • 订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
  • 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?

如果不控制幂等,那么:
比如订单提交的过程中,用户点了一次提交,但是由于网络等原因,导致后端处理延时,客户就连 续点击了多次,在没有幂等性的条件下,那么就会造成订单的重复提交。

解决思路:在保存订单的时候,根据生成的系统全局唯一ID(这里订单号+业务类型),并且把该唯一ID 调用 redis 的setnx命令保存起来,在第一次保存的时候,由于redis中没有该key,那么就会 把全局唯一ID 进行设置上,此时订单就会保存成功。
这个时候若出现前端重复点击按钮, 由于第一步已经 setnx上了 就会阻止后面的保存。
即分布式锁的思想

2. mq消息幂等性

在这里插入图片描述

第一步:消息生产者向Mq服务端发送消息
第二步:mq 服务端把消息进行落地
第三步:消息服务端向消息生产者发送ack
第四步:消息消费者消费消息
第五步:消费者发送ack
第六步: mq服务将落地消息删除

消息重复发送的原因

为了保障消息的百分之百的投递,我们使用了消息重发-确认机制,导致消息可能被重复发送,由上图可知道,由于网络原因,第三步的上半场ack丢失还是第五步的下半场ack丢失 都会导致消息重复发送

mq服务端(broker)是如何保证幂等性的?

消息队列的服务中,对每一条消息都会生成一个全局唯一的与业务无关的ID(inner_msg_id)。
当mq_server 接受到消息的时候,先根据inner_msg_id 是否需要重复发送给消费端,再决定消息是否落DB ,这样保证每条消息都只会落一次DB。
即broker可以判断inner_msg_id是否重复来判断是否持久化到硬盘里。即通过DB来控制幂等。

消费端如何来做到幂等性的?

还是把对每条消息做生成一个唯一性的ID 通过redis的来setnx命令来保证幂等性。
即通过分布式锁控制幂等。

3.幂等性解决方案

1 分布锁
适用于高并发场景,基于redis实现唯一键的读写,性能快支撑大流量高并发

2 DB自带的唯一索引机制

3.前端控制不让重复提交

上面三种方案并不是互斥的,而是相互结合的。首先前端控制不让重复提交是第一道防线,分布式锁解决高并发场景是第二道防线,当前面两种控制都失效,则DB唯一建控制则是最后一道防线了,他能保证数据的绝对唯一性。

2.4 轮询

1.短轮询

定义:其实就是普通的轮询。指在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。如果服务端没有数据,将一直重试请求。
类似于Java中自旋锁方式。

应用场景:传统的web通信模式。后台处理数据,需要一定时间,前端想要知道后端的处理结果,就要不定时的向后端发出请求以获得最新情况。

优点:前后端程序编写比较容易。

缺点:请求中有大半是无用且难于维护,浪费带宽和服务器资源;

响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。

实例:适于小型应用。

2.长轮询

定义:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
类似于Java中wait于notify模型。

优点:在无消息的情况下不会频繁的请求,耗费资源小。

缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。

实例:WebQQ、Hi网页版、Facebook IM。

3.长轮询和短轮询比较

两者相同点:
可以看出 http 长轮询和 http 短轮询的都会 hold 一段时间;

两者不同点:
间隔发生在服务端还是浏览器端: http 长轮询在服务端会 hold 一段时间, http 短轮询在浏览器端 “hold”一段时间;

总结:可以使用超时机制权衡短轮询和长轮询

2.7 通信协议

1.RESTful

REST可以看着是http协议的一种直接应用,默认基于json作为传输格式,使用简单,学习成本低,效率高,但是安全性较低.

RESTful是一种架构的规范与约束原则,符合这种规范的架构就是RESTful架构。
RESTful大部分协议都是基于HTTP协议的。

@see RESTful 架构详解 https://www.runoob.com/w3cnote/restful-architecture.html

2.webservice

基于xml为格式的传输协议。在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的。

3.RPC

是一种允许分布式应用程序调用网络上不同计算机的可用服务的机制,一般通过tcp协议进行传输。

通常和分布式框架结合使用如dubbo,通常包含了负载均衡,集群容错等高可用,高性能特点但学习和维护难度大。

思考

1.http和tcp的区别?

1、性质不同:

TCP对应与传输层、而HTTP对应于应用层,所以HTTP协议是建立在TCP协议之上的;

2、TCP是网络传输协议, HTTP是超文本传输协议;
TCP是底层协议,定义的是数据传输和连接方式的规范。
HTTP是应用层协议,定义的是传输数据的内容的规范。

3.HTTP是无状态的短链接,而TCP是有状态的长连接;

总结:
Http协议是建立在TCP协议基础之上的。当浏览器需要从服务器 获取网页数据的时候,会发出一次http请求。

Http通过TCP建立起一个到服务器的通道。

当一个网页完成之后,客户端和服务器端之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个页面时,会继续使用这一条已经建立的连接Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件中设定这个时间,

10 长连接和短连接

短连接:
例如普通的web请求,在三次握手之后建立连接,发送数据包并得到服务器返回的结果之后,通过客户端和服务端的四次握手进行关闭断开。

长连接:
区别于短连接,由于三次握手链接及四次握手断开,在请求频繁的情况下,链接请求和断开请求的开销较大,影响效率。采用长连接方式,执行三次握手链接后,不断开链接,保持客户端和服务端通信,直到服务器超时自动断开链接,或者客户端主动断开链接。

分布式难点
1.数据一致性

跨域

什么是跨域

浏览器对于javascript的同源策略的限制,例如a.cn下面的js不能调用b.cn中的js,对象或数据(因为a.cn和b.cn是不同域),所以跨域就出现了.

上面提到的,同域的概念又是什么呢??? 简单的解释就是相同域名,端口相同,协议相同。

如何跨域

1.jsonp跨域

凡是拥有scr这个属性的标签都可以跨域例如

jsonp:

jsonp 全称是JSON with Padding,是为了解决跨域请求资源而产生的解决方案,是一种依靠开发人员创造出的一种非官方跨域数据交互协议。

思考

如何设计一个良好的API

自己总结:

1.入参使用对象的形式接受参数,这样往里面加参数时,不会影响其他接口。对参数作校验,对应批量接口,要限制个数。

2.返回参数也使用对象形式,不是使用map,便于冗余数据

3.对应无法共用的接口,注释需要标明调用业务方,底层的能尽量共用,上层可以渐渐分化。

4.返回状态码约定

5.身份认证避免非法连接

如何设计一个良好的消费API的消费端

自己总结:

1.异常捕获,并打印关键日志。不能因为调用异常,而中断整个服务

2.网路超时

3.条件允许加入熔断机制

参考资料

1.什么是分布式系统,如何学习分布式系统:https://www.cnblogs.com/xybaby/p/7787034.html

2.深入浅出SOA https://www.cnblogs.com/renzhitian/p/6853289.html

3.初步理解一下:SOA, SOAP, Web Service, WSDL等https://www.cnblogs.com/yuxiang204/archive/2012/10/16/2726006.html

4.如何正确理解CAP理论?https://www.jdon.com/bigdata/how-to-understand-cap.html

5.从分布式一致性谈到CAP理论、BASE理论https://www.cnblogs.com/szlbm/p/5588543.html


http://www.mrgr.cn/news/79597.html

相关文章:

  • 使用mmdeploy框架C++预测mask并绘制最小外接矩形
  • 排序算法(1):冒泡排序
  • STM32F103 FPGA进行通信方式
  • 第四十六篇 Vision Transformer论文翻译
  • 【开源】A065—基于SpringBoot的库存管理系统的设计与实现
  • java中的抽象类
  • Redis安装和Python练习(Windows11 + Python3.X + Pycharm社区版)
  • 人工智能大模型LLM开源资源汇总(持续更新)
  • 【光电倍增管】-打拿极 PMT
  • SpringBoot3整合Druid数据源
  • 配置新电脑设置的脚本
  • 嵌入式入门Day26
  • android NumberPicker隐藏分割线或修改颜色
  • android notification
  • Python 检验项目分析与历次报告比对
  • SpringBoot3整合SpringMVC
  • 制造业信息化系统:构建高效生产与管理的数字化基石
  • 阿里云 云产品流转(实现设备与小程序交互)
  • c++ 学习笔记 函数进阶
  • Python知识分享第二十二天-数据结构入门