TCP协议

TCP特点:

有连接,可靠传输,面向字节流,全双工.

①端口号(传输层概念):

是一个重要的部分,知道了端口号,才能进一步的确认这个数据报交给哪个应用程序. 

②4位首部长度:

  • TCP的报头是"变长"的.
  • 4bit表示的范围是:0->15,此处的单位,是"4字节",把这里的数值*4,才是真正的报头长度.
  • TCP报头,最大长度,60字节.
  • TCP报头的前20个字节,是固定的.TCP报头的最短长度,20字节.
  • 使用首部长度,来确认,报头到哪里就结束了,载荷数据是从哪里开始的.

③标志位:

这里是六个标志位.

给未来留下了可以升级拓展的空间~

1.确认应答(可靠性)

1.1 引入 

当连续发多条数据的时候,这多条数据,可能会出现"后发先至"的情况.

即一个数据报,是先发的,但后发的反而先到了.

网络上,从A->B中间的路径有很多,两个包,从A->B的路线不一定相同!!!

另外,每个节点(路由器/交换机)的繁忙程度不同,此时,这样的转发过程,就会存在差异,就和等红灯一样.

那怎么办呢??

针对数据进行编号!!!

1.2 确认应答机制(可靠性机制) 

TCP是面向字节流的,不是按照"条"为单位来传输的.

第一条1-1000  也可以进行调整(1-1500)

第二条1001-2000

注意:

①TCP是针对字节进行编号的,而不是针对"条".

②应答报文也是要和收到的数据的序号相关联的,但不是"相等".

  • 只要知道这一串字节的开始编号,以及数据的长度,每个字节的编号就知道了.
  • 只需要在TCP报头中,把这一串字节第一个字节的编号(32位序号),表示出来,再结合报文长度,此时每个字节的编号就确定了.
  • 确认序号的数值,就是收到的最后一个字节的编号+1.

问:

那么如何区分,一个报文是普通报文还是确认应答报文?

要看六个标志位,ACK

ACK为0,表示是一个普通报头,此时只有32位序号,是有效的.

ACK为1,表示是一个应答报文,这个报文的序号和确认序号都是有效的.

确认报文的序号和正常报文的序号,没有关联关系.

序号,是自己的这个主机,发送的数据进行的编号.

确认应答,是TCP保证可靠性的最核心机制!!!

超时重传,是TCP可靠性机制的有效补充.

2.超时重传(可靠性)

2.1 引入 

丢包:网络上,很可能出现,发一个数据,然后丢了.

  • 结构复杂,传输的数据量也是不确定,一会数据少,一会数据多.
  • 如果设备太繁忙了,后面新来的数据等太久了,就可能被丢弃了.
  • 网络负载越高,越繁忙,就越容易丢包. 

那该怎么办?!

如果丢包了,我收不到应答(等待了一定时间,超过了一定时间),就要尝试重传,这就是超时重传.

超时重传,相当于针对确认应答,进行的重要补充.

2.2 超时重传相关问题

2.2.1 如果应答报文丢包了,我重传,会造成问题吗?

举个例子,我充值的时候,充了10块,由于重传, 就成了充值20块.

所以,接收方收到数据之后,就需要对数据进行去重,把重复的数据丢弃掉,保证应用程序,调用inputStream.read时,读到的数据不会出现重复.

2.2.2 如何进行去重呢?如何高效的判定当前收到的数据是否重复呢?

直接使用TCP的序号来作为判定依据.

TCP会在内核中,给每个Socket对象都安排一个内存空间,相当于一个队列,也称为"接收缓冲区".收到的数据,都会被放到接收缓冲区,并且按照序号排列好顺序了.此时就可以很容易的找到该新收的数据是否重复了.(后发先至问题,在这里也解决了)

因此,应用程序,就不必考虑数据传输先后顺序问题.

这里其实也是一个生产者消费者模型

收到数据,接收方的网卡,把数据放到相应的Socket的接收缓冲区中(内核中),应用程序,调用read,就是从这个接收缓冲区,消费数据.

当数据被read走了,就可以从对列中删除掉了(read的时候,有两种模式,可以读到就删除[默认],也可以读到不删除).

2.2.3 为什么重传的时候,数据就能传过去?

丢包本质上是一个"概率性"问题.

假设丢包的概率是10%,成功的概率是90%.

连续两次传输,都丢包的概率为10%*10%=1%.

随着重传次数的增加,总体能够传输成功的概率,是更大的.

当然,也存在连续重传多次仍旧丢包的情况.

2.2.4 等待超时时间,是等多久呢?

超时时间不是一个固定的值,会随着超时轮次的增加,而进一步增加.

随着重传轮次的增加,等待时间也会越来越长.

当前认为,正常情况下,第二次重传就有极大的概率重传成功,再不济,第三次也有更大的概率成功.

如果这几次都没成功,说明当前网络本身的丢包概率就已经极高了,此时进行频繁的重传,就是白费力气.拉长一些时间间隔,也是在给网络恢复留有一个时间上的余地.

当然,超时重传的轮次,也不是无限的.重传次数达到一定程度,就放弃了,此时就会尝试"重置"TCP连接,即为TCP复位报文.

六个标志位中的RST:

为1表示是一个复位报文.

如果网络已经出现严重故障,复位操作/重置操作,也无法成功,最终只能放弃连接.

3.连接管理(可靠性)

1.建立连接——三次握手

2.断开连接——四次挥手

3.1 三次握手

握手:发一个打招呼的数据(并不会携带业务信息),使用打招呼来触发"特定场景".

A和B完成建立连接的过程,就需要"三次"这样的打招呼的数据交互.

这样的四次交互完毕之后,连接就算建立好了,此时双方就已经保存了对端的信息了.

实际上中间这两次,能够合并为一次!

  • 为什么要合并呢?

封装和分用!!!

合并之后,节省了封装和分用的过程,降低了成本,提高了效率.

原则上,能合并就合并.

  • TCP三次握手过程:

SYN:

申请建立连接的请求,"同步报文段".

SYN为1,就是"同步报文段",就是一台主机尝试和另一台建立连接.

第一次的SYN一定是客户端发起的(主动的一方).

  • 服务器在处理第一个客户端的时候,虽然无法调用第二次accept,但是不影响内核的工作,内核已经和第二个客户端三次握手完成了,形成的连接对象是在连接队列里(Socket).
  • 连接已经形成了,但是在队里里暂时没人取,accept工作只是把队列中的元素取出来.
  • 三次握手的意义?

也是一种保证可靠性的机制.

举个例子:

地铁每天早上第一班车不拉客:看看线路是否有故障.

TCP要想保证可靠传输,前提是网络路径畅通.

TCP三次握手,就是要验证网络通信是否畅通,以及每个主机的发送能力和接收能力是否正常.

此时,麦克风:发送能力   耳机:接收能力

这些基础设施的正常,是后续进行可靠传输的前提条件.

  • 那么为什么是三次握手,两次呢?四次?

很明显,两次不可以,虽然两次握手,是可以验证通信能力的正确性,但服务器这边还不知道这样验证通过的信息.

四次没必要,能合并就合并.

恰好三次,就能验证双方的发送和接收能力均正常,并且把这样的信息同步给双方.

  • 三次握手,可以起到"消息协商"的效果.

通信的时候涉及到一些参数,需要双方保持一致的通过协商,来确定参数具体是多少.

举个例子,办酒席的时候,需要双方协商各自的桌数.

TCP通信过程中,有很多信息是需要进行协商的.比如说双方的序号从几开始.(一般不会从0/1开始)

这样做注意就是保证,两次连接,消息的序号能够有较大的差异.从而去判定某个消息是否属于这个连接.

网络上传输的消息,可能后发先至,极端情况下,某个消息,迟到了很久,当消息到达对端的时候,服务器和客户端已经断开了上一个连接,这是重新建立的连接了.这个时候,就可以通过序号,明显的识别出这个是上一个连接的消息,就可以丢弃了.

  • 综上,三次握手的意义:

①投石问路,验证通信路径是否畅通,双方的发送/接收能力是否正常.

②协商必要的参数,使客户端和服务器使用相同的参数进行消息传输.

3.2 四次挥手

连接,通信双方各自在内存中保存对端的相关信息,不需要连接,就得及时的释放上述存储空间.

三次握手,必然是客户端主动发起的,但是四次挥手不一定,大多时候还是客户端发起的.

FIN:

六个标志位的一个

为1,就是结束报文段.

经过上述四个步骤之后,连接就彻底不再使用了,双方就可以把各自保存对端消息的空间释放了.

  • 四次挥手,三次可以吗?

有的时候可以三次,有时候不能.

中间这两次,不一定能合并.

FIN的触发,是应用程序代码来控制的,调用socket.close(),或者进程结束,就会触发FIN.

相比之下,ACK是内核控制的,收到FIN就会立即马上返回ACK.

  • 那么如果服务器,始终不进行close,会这么样?客户端的连接就始终不关闭吗?

 

此时,服务器的TCP状态,就处于CLOSE_WAIT状态.

如果没进行CLOSE_WAIT——>LAST_ACK这样的转换,就仍然是CLOSE_WAIT.

站在服务器的角度,虽然这里的连接没有关闭,但是这个连接其实已经不能正常使用了.

针对当前socket进行读操作,如果数据还没读完(缓冲区还要数据),能正常读到.

如果数据已经读完了,就会读到EOF(对于字节流来说,返回-1,对于scanner,hasNext就会是false).

针对当前socket进行写操作,直接就会触发异常.

总之,无论如何,对于这个连接,关闭是唯一的选择.

更极端的情况下,比较代码出现BUG,close就忘记写了,此时站在客户端的角度,这边迟迟收不到对方的FIN,也会进行等待,如果一直等,等不到.此时就会单方面放弃连接(客户端就把自己保存的对端的信息删了,释放了).

目标,释放资源.能双方都顺利释放,固然最好.如果条件不允许,也不影响单方面释放.

  • 如果通信过程中,出现丢包了,怎么处理?

三次握手,四次挥手,也都是带有重传机制的.

尽可能重传,如果仍旧失败,连续多次,此时就单方面释放连接.

  • 站在A的角度,当收到这个FIN之后,并且发出去ACK了,此时A就视为四次挥手结束了吗?就可以直接释放连接了吗?

不是!!!最后一个ACK可能丢包!!

如果最后一个ACK丢失,B就会重新传过来一个FIN,如果此时A已经把连接释放了,那重传的FIN就无人能够进行ACK了.

因此,就需要让A在发出去最后一个ACK之后,让连接在等一会(主要是看等待的过程中会不会收到对方重传的FIN),如果等了一定时间之后,对方还没有重传FIN,就认为ACK已经被对方收到了,此时A才能正确释放连接.

  • 那么A这边等待多久才能释放连接呢?

等待时间就是网络上任意两点之间传输数据的最大时间*2(定义为MSL)

超时重传的时间必然<=MSL.

如果超时时间达到MSL上限,此时100%包已经丢了,超时时间设置的更大,也没有意义了.一般来说超时时间肯定是更短的.

极端情况:

比如,A在等2MSL时间的过程中,B在反复重传FIN多次,但都丢了.如果真出现这个情况,当前这个网络一定出现了严重故障.这个时候,已经不具备"可靠传输"的前提条件了,因此,A就单方面释放资源.

 

4.滑动窗口(效率)

作用:提高传输效率(更准确的说,让TCP在可靠传输的前提下,效率不要太低)

使用滑动窗口,不能使TCP变的比UDP快,但是可以缩小差距.

4.1 窗口引入 

没有滑动窗口,会带有确认应答.这样的传输,能够保证可靠性,但是大量的时间,都消耗在等ACK上了.

使用滑动窗口,就是为了缩短上述等待时间.

一次发出一组数据,发这一组数据的过程中,不需要等待ACK,就直接往前发.

此时,就相当于使用"一份等待时间",等四个ACK.

把一次发多少数据,不用等ACK这样的大小,称为"窗口".

窗口越大,此时批量发送的数据就越多,效率就越高.

但是窗口不能无限大,如果无限大,相当于完全不必等ACK,此时就和不可靠传输差不多了.

如果无限大,接收方能不能处理过来?中间的网络设备能否承受的住?都是未知数.

4.2 滑动

上述我们知道了什么是窗口,那"滑动"呢?

在上图中,当前A给B批量发送了

1001-2000,2001-3000,3001-4000,4001-5000

这样的数据就需要等4个对应的ACK.

这4个ACK到达顺序,也会有先有后.

比如,就是按照2001,3001,4001,5001这种顺序到达,那么2001到达主机A后,A是否要继续往下发下一条消息?(这样仍然是等待四个ACK),还是说A需要等待5001到了,才能继续发下一组消息(总的等待时间太长了,没必要).

滑动窗口,是一个形象的比喻,实际上本质是批量发送数据.

这样就可以缩短等待时间,比之前提升一定的效率.

当然缩短不等于没有,仍然需要花时间等待,传输效率不会高于UDP.

4.3 丢包

那按照这种批量的方式传输,中间丢包了怎么办?

对于TCP来说提高效率,不然不能影响到可靠性!!!

两种情况:

①数据丢了

②ACK丢了

4.3.1 ACK丢了

如果ACK丢了,不用做任何处理,也是正确的!!!

2001确认序号,表示2001之前的数据都收到了,也包含1-1000.

虽然A没有收到1001这个ACK,但是2001这个ACK涵盖了1001的含义.

除非是所有的ACK都丢了(网络出现重大故障),否则,只丢一部分ACK,对于可靠传输没有任何影响.

4.3.2 数据报丢了

在1001-2000丢失之后,2001-3000这个数据到达了B,B返回的ACK确认序号,仍然是1001.B仍然再向A索要1001这个数据.

虽然A后续给B的数据都顺利传过去了,但是只要1001这个数据没有,B始终会向A索要1001这个数据,返回的ACK确认序号,都是1001.

当A连续几次都收到了来自于B的索要1001的数据,A就明白了,1001是丢了.

A就会重新传输1001-2000这个数据.

当重传的1001到达B之后,B返回的ACK就是7001了.

此时,就相当于使用最小的成本,来完成了这个重传数据的操作.

只是把丢的数据重传了,其它的数据都没有重复操作.

形成了快速重传,本质上还是超时重传,是在结合滑动窗口下,产生的变形操作.

  • 滑动窗口,也不是说,使用TCP就一定会涉及:

如果通信双方大规模传输数据,肯定是滑动窗口(此时按照快速重传来工作).

如果通信双方传输数据规模比较少,这个时候就不会滑动窗口了(仍然按照之前的超时重传来工作).

5.流量控制(可靠性)

是滑动窗口的补充.

刚刚提到的滑动窗口,窗口越大,传输效率越高.

但是窗口不能无限大,如果太大,就可能使接收方处理不过来了,或者是使传输的中间链路出现处理不过来.

这样就会出现丢包,就得重传了,窗口大并没有提高效率,反而还影响了效率.

流量控制,就是给滑动窗口,睬踩刹车,避免让窗口太大,导致接收方处理不过来.

因此,流量控制,就是根据接收方的处理能力,来限制发送方分发送速度(窗口大小).

  • 那如何衡量接收方的处理速度?

使用接收缓冲区剩余空间大小来作为衡量指标.

如果剩余空间越大,应用程序消费数据的速度就越快.

此处,就会直接把接收缓冲区剩余空间大小,通过ACK报文反馈给发送方,作为发送方下一次发送数据,窗口大小参考依据.

虽然A不再发送数据了,但是也不知道B这边什么时候能腾出空间,就会周期性的发送"窗口探测包"(不携带具体数据),只是为了触发ACK,去查询当前接收缓冲区的情况.

一旦发现不是0了,就可以继续发送了.

接收方就可以根据窗口大小,来反向限制发送方传输速度了.

当然,只考虑接收方,是不够的,还要考虑中间链路的处理能力.

6.拥塞控制(可靠性)

 总的传输效率,相当于一个木桶效应,取决于最短板.

虽然中间的设备,一般是运营商提供的企业级的路由器/交换机,整体的转发能力是非常强的.但是也架不住量大.(在宽的路,车多也会堵车)

  • 那么怎么去衡量中间设备的转发能力呢?

此处,并不会针对中间设备的转发能力进行量化.

而是把中间设备都看成一个整体.

采取"实验"的方式,动态调整,产生出一个合适的窗口大小.

①使用一个较小的窗口传输,如果传输通常,就调大窗口.

②使用一个较小的窗口传输,如果传输丢包(出现堵塞),就调小窗口.

这样做可以非常好的适应网络环境的动态变化.

6.1 拥塞窗口

在拥塞控制机制下,采用的窗口大小.

具体展开:

6.1.1 慢启动

刚开始进行通信的时候,会使用一个非常小的窗口,先试试水.

(如果网络拥堵,一上来就搞个很大的流量,就雪上加霜了)

6.2.2 指数增长

在传输通畅的过程中,拥塞窗口就会指数增长(*2).

(指数增长速度是极快的,不能不加限制,否则就会出现非常大的值)

6.3.3 线性增长

指数增长当拥塞窗口达到一个阈值之后,就会从指数增长,转换成线性增长.

(线性增长,也是增长,就会使发送速度,越来越快.快到一定程度,接近网路传输的极限,就可能会出现丢包了)

这里的指数增长和线性增长,都是按照传输的轮次.

假设给定窗口大小为4000,发完了这个数据之后,这一轮就结束了,当收到ACK之后,继续往下发数据,进入下一轮.

6.4.4 拥塞窗口回归小窗口

窗口大小增长过程中,如果传输出现丢包,认为当前网络出现拥堵了.

此时就会把窗口大小调整为最初的小窗口,继续回到之前的指数增长+线性增长的过程.

另外此时也会根据当前出现丢包的窗口的大小,调整阈值(指数增长->线性增长).

新的阈值:出现丢包的窗口大小/2.线性增长就从这个新的阈值开始.

拥塞窗口,就是在这个过程中不断发生变化,不断重新调整的过程.这样的调整可以更好的适应多变的网络环境.

当然,每次回到慢开始这里,都会使传输速度大打折扣.有不少的性能损失.

因此,拥塞控制这里后续也衍生了一些优化算法,尽可能地把小窗口地传输时间缩短.

  • 综上:

此处不仅要考虑接收方地处理能力,更要考虑中间节点地处理能力.

所以实际发送的窗口= min(拥塞窗口,流量控制窗口).

拥塞控制和流量控制,共同限制了滑动窗口机制,可以使滑动窗口,在可靠性的前提下,提高传输效率.

 

7.延迟应答(效率)

提高传输效率的机制,是围绕滑动窗口琢磨的.

  • 那能否在条件允许的基础上,尽可能的提高窗口大小呢?

需要在返回ACK的时候,拖延一点时间,利用这个拖延的时间,就可以给应用程序腾出更多的消费数据的时间,这样接收缓冲区的剩余空间,就会更大了.

 当然,这里通过延迟应答,更提高多少速度,还是取决于接收方应用程序的实际的处理能力.

  • 那么所有的包都可以延迟应答嘛?

不是!!!

①数量限制:

针对于滑动窗口,每隔N个包就应答一次.

②时间限制:

针对于非滑动窗口,超过最大延迟时间就应答一次.

8.捎带应答(效率)

在延迟应答的基础上,引入的进一步提高效率的方式.

延迟应答,是让ACK传输的时机更慢.

捎带应答,是基于延迟应答,让数据进行合并.

客户端和服务器之间的交互,主要是一问一答的形式.

之前提到四次挥手可能是三次,主要就是延迟应答和捎带应答起到的效果.

数据报从两个合并成一个,效率会有明显的提升.

主要还是因为这里每次传输数据都是需要封装分用的.

能合并的原因,一方面是时机上可以同时,另一方面,ACK数据本身不需要携带载荷,和正常的数据也不冲突.

完全就可以让这一个数据报,既能携带载荷数据,又能带有ACK的信息.(ACK标志位,窗口大小,确认序号)

9.面向字节流=>粘包问题(编程注意事项)

在面向字节流的情况下,会产生一些问题.

粘包问题:

这里粘的是"应用层数据报".

通过TCP read/write 的数据,都是TCP报文的载荷,也就是应用层数据.

发送方一次性是可以发送多个应用层数据报的.

  • 但是接收的时候,从哪里到哪里是一个完整的应用数据报?

发送两个包,读了半个,一个半,都会产生问题.

在发送方的角度,预期:

aaa是一个应用层数据报.

bbb是一个应用层数据报.

ccc是一个应用层数据报.

但接收方的应用程序,读取这里的数据时,read可能是一次读一个字节,或者一次读若干个字节.没有办法一次读一个应用层数据报,这时就出现了粘包问题.

那该怎么做呢?

正确的做法是,合理的设计应用层协议.

这件事在传输层这边已经无解了.需要站在应用层的角度,来解决这个问题.

9.1 解决方法

1.应用层协议中,引入分隔符,区分包之间的边界.

2.应用层协议中,引入"包长度",区分包之间的边界.

9.1.1 使用\n约定包之间的分隔符

使用什么符号作为分隔符都是可以的.

之前Echo Server就是这么设定的:

发送的时候,使用println来发送,一定会以\n结尾.

接收的时候,使用scanner.next来解析,遇到\n就返回. 

9.2.2 使用包的长度来区分

当然,粘包问题,不仅仅是TCP才有的.

只要是面向字节流的机制(文件)也有同样的问题. 

当然,解决方案也一样,使用分隔符,或者使用长度.

尤其是在自定义应用层协议的时候,就可以使用这样的思想来解决问题了.

xml/json是通过分隔符来区分包的.

protobuffer是通过长度来区分包的.

10.TCP异常情况的处理(异常情况)

网络本身就会存在一些变数,导致TCP连接不能继续正常工作了.

10.1 进程崩溃

进程崩溃=>PCB没了=>文件描述符表也就被释放了=>相当于调用了socket.close( )=>崩溃的这一方就会发出FIN,进一步触发四次挥手,此时连接就被正常释放了.

socket在系统内核中也是一个文件,也会被放到文件描述表中.

此时,TCP的处理和进程正常退出.

10.2 主机关机(正常步骤的关机)

正常关机,会先尝试关掉所有的进程(即强制终止进程),和上诉崩溃的处理是一样的.

主机关机会有一定的时间,在这个时间之内,四次挥手可能会正好挥完,但没有挥完,也不要紧.

10.3 主机掉电(拔电源,此时没有任何反应的空间)

拔电源,电脑瞬间就黑了,此时就没有任何可以操作的空间了.

A无法给B发起任何的FIN.

10.3.1 如果B正在给A发消息(接收方掉电)

这个情况简单一点,B发的消息,没有ACK了,B就会触发超时重传.

重传仍然失败,就会触发复位报文(RST字段=1),尝试重置连接.

重置操作仍然失败,此时就会继续单方面释放连接了.

B没什么负面影响.

10.3.2 如果A正在给B发消息(发送方掉电)

这个情况复杂一些,B在等A的消息,A突然就不发了,B不知道A是否会继续发,B就会阻塞等待,具体等多久,也不知道.

此处就涉及到"心跳包",B这边虽然是接收方,也会周期性的给对方发起一个不懈带任何业务数据(载荷)的TCP数据报.目的就是为了触发ACK,确认一下A是否正常工作/网络是否畅通.

"心跳包"和上诉的流量控制"窗口探测报文"类似.就比如打电话的时候,一方突然没声音了,另一方就会"喂喂"的试探.

虽然TCP中已经有心跳包的支持了,但是还不够,往往还需要在应用层,应用程序中重新实现心跳包.

TCP的心跳包,周期太长了,分钟级别的,而现在告发场景下,分钟级是不够的,需要秒级甚至毫秒级,可以在更短的时间内,发现某个服务器出现问题.

10.4 网线断开

相当于主机掉电的升级版本.

此时A和B就无法通信了.

A这边发生的情况,就是主机掉电的第一种情况.

B这边发生的情况,就是主机掉电的第二种情况.

11.TCP与UDP 对比

TCP:

优势在于可靠性,适用于绝大部分场景.

UDP:

优势在于效率,适用于机房内部的主机之间通信(机房内部,带宽比较充裕,不容易遇到拥堵丢包的情况,主机之间通信速度能比较快).

当然,当既要实现可靠传输,又要效率的游戏场景下,使用TCP和UDP都不一定合适.

比如KCP这样的一些协议.

其它所谓的"传输层协议"并非真正的工作在传输层,往往是在应用层实现的类似于传输层的机制,同时在底层是基于UDP来完成的(基于UDP实现可靠传输,这里也会涉及到一些确认应答,超时重传,滑动窗口......(参考TCP),但是此处会更侧重于效率).

[经典面试题]

如何基于UDP实现可靠传输?

这个问题本质考察的还是TCP.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值