【Linux】【网络】传输层协议:TCP

TCP 协议

TCP(Transmission Control Protocol 传输控制协议)

  • 传输层协议。
  • 有连接:处于通信之前,也就意味着三次握手是不携带有效信息的。
  • 可靠传输:有确认机制如 收到应答、超时重传、三次握手、四次挥手…
  • 面向字节流:有对应的以字节为单位的缓冲区收发数据以供解析。

对比 UDP:

UDP(User Datagram Protocol 用户数据报协议)传输的过程类似于寄信。
- 传输层协议。
- 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接。
- 不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
- 面向数据报: 不能够灵活的控制读写数据的次数和数量。

1. TCP 协议段格式

TCP 的首部有一块定长大小为 20 字节的空间字段,用于报头信息的填写。后面接着的是一块储存选项的空间,也可能没有选项信息。

既然不确定选项字段的情况,如何分离报头和有效载荷呢?

  • 在 TCP 前 20 字节的定长字段之中有一个记录首部长度的空间,这部分空间大小是 4 个比特位,而其中记录的单位大小是 4 字节,即可以记录 60 字节的长度,前 20 是定长的,后 40 就是给头部选项的。

  • 所以整个首部的大小 = 首部长度字段 * 4 - 20,如果 == 0,则报头读取完毕,剩下的就是有效载荷
    在这里插入图片描述


2. TCP 报头解析

源 / 目的端口号

  • 表示数据是从哪个进程来,到哪个进程去

32 位序号 / 32 位确认号

  • 对报文的编号,和确认报文的编号(见 TCP 的可靠性)

4 位 TCP 报头长度

  • 4个比特位能记录的数据为 0000 到 1111,换成十进制也就是0~15,其中单位 1 表示 4 个字节大小。整体4 位 TCP 报头长度的填充值如果为 x,表示该 TCP 头部所占空间为 4x;所以可以推出,TCP头部最大长度是15 * 4 = 60 字节

6 个重要标志位

  • ACK - - acknowledge:该报文是一个确认报文,确认报文可能会携带数据,携带数据的确认报文的应答叫做 捎带应答。

  • SYN – sync:请求建立连接,我们把携带 SYN 标识的称为 同步报文段

  • FIN:请求断开连接,通知对方,本端要关闭了,我们称携带 FIN 标识的为 结束报文段

  • RST - - reset:对方要求重新建立连接,我们把携带 RST 标识的称为复位报文段。

  • PSH - - push:提示接收端应用程序立刻从 TCP 缓冲区把数据读走。

  • URG - - urgent:标识紧急指针字段是否有效。

16 位窗口大小

  • 发送自己端接收缓冲区的剩余空间大小,以作 流量控制,不会导致对方发太快太多导致丢包,也可以避免发的太慢效率低下的问题出现。

16 位校验和

  • 发送端填充,CRC 校验。接收端校验不通过,则认为数据有问题,直接丢弃。此处的检验和不光包含 TCP 首部,也包含 TCP 数据部分。

16 位紧急指针

  • 是一个偏移量,标识哪部分数据是紧急数据(一个字节的状态码,这样的数据也叫做外带数据)。

40 字节头部选项:见后文。

其中 紧急数据 的处理,可以在 recv 和 send 中设置:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数 flags:

  • MSG_OOB:允许 接收 / 发送 非正常流中的 紧急数据
但在实际使用中 URG 使用的很少,因为他能携带的信息量实在有限,
更多的是多开一个端口号,拿到并处理特殊信息

3. TCP 的可靠性

不可靠的情况有很多,比如:丢包、乱序、重复、校验失败、发送太快/太慢、网络问题…

客户端发送请求,在收到服务器响应,才可以 100% 的确保对方是收到的,也就是说 可靠性,是通过收到应答机制保证的。如此也说明,我们无法保证任何报文都是可靠送达,但可以局部保持可靠性。

实际上客户端可以同时对服务器发送多个请求报文,经过网络的传输,到达服务器的情况却不一样,应对不同的报文送达情况,客户端有不同的应对机制,判断哪个报文是哪个报文就是很有必要的。所以报文里会携带两种编号:序号、确认序号。确认序号是收到序号 +1 来设定的。两个序号在一段报文中注定是更高效的,所以在报头中有各自不同的字段空间。

序号的设置保证了 TCP 的可靠性。

4. 面向字节流

创建一个 TCP 的 socket,相当于同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区

  • 调用 write 时,数据会先写入发送缓冲区中。

  • 如果发送的字节数太长,会被拆分成多个 TCP 的数据包发出;如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。

  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区。

  • 然后应用程序可以调用 read 从接收缓冲区拿数据。

  • 另一方面,TCP 的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工

由于缓冲区的存在,TCP 程序的读和写不需要一一匹配,例如:

写 100 个字节数据时,可以调用一次 write 写 100 个字节,也可以调用 100 次 write,每次写一个字节。
读 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节,也可以一次 read 一个字节,重复100次。

5. 粘包问题

首先要明确,粘包问题中的 “包”,是指的应用层的数据包。

在 TCP 的协议头中,没有如同 UDP 一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。

  • 站在传输层的角度,TCP 是一个一个报文过来的。按照序号排好序放在缓冲区中。
  • 站在应用层的角度,看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。

避免粘包问题, 归根结底就是, 明确两个包之间的边界

  • 对于定长的包,保证每次都按固定大小读取即可。例如上面的 Request 结构,是固定大小的,那么就从缓冲区从头开始按 sizeof(Request) 依次读取即可。

  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。

  • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)

对于 UDP 协议来说,是否也存在 “粘包问题” 呢?

  • 对于 UDP,如果还没有上层交付数据,UDP 的报文长度仍然在。同时,UDP 是一个一个把数据交付给应用层。就有很明确的数据边界。
  • 站在应用层的站在应用层的角度,使用 UDP 的时候,要么收到完整的UDP报文,要么不收,不会出现 “半个” 的情况。

6. 连接队列维护

Linux 内核协议栈为一个 TCP 连接管理使用两个队列。

  1. 半链接队列(用来保存处于 SYN_SENT 和 SYN_RECV 状态的请求)
  2. 全连接队列(accepted 队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)

全连接队列:管理已连接但还没发起任务的客户端。为了保证服务器的使用率,全连接队列的维护是必须维护的。也要控制这个队列不能太长。

  • 因为可能会过多消耗 OS 本来是给 server 使用的资源,且队列太长 client 会不愿意等。
  • 实际上更有效率的应该是 提高 网络吞吐量。
  • 至于队列究竟要维护多长,需要看各自应用场景。

半连接队列:管理半连接状态的队列。

TCP 协议,需要在底层维护 全连接队列,最大长度是:listen() 的第二个参数 +1


TCP 的 确认应答机制

在这里插入图片描述

在确认应答机制中,TCP 首先将每个发出的数据,在发送缓冲区中都以字节为单位进行了编号,即序列号。每一个 ACK 都带有对应的确认序列号(序列号 +1),意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。

例如,主机 A 和主机 B 通信:
A to B:发送数据(1~1000)
B to A:确认应答(收到了,下一个从 1001 开始发)
A to B:发送数据(1001~2000)
B to A:确认应答(收到了,下一个从 2001 开始发)
...

在字节单位的缓冲区中不断的读取和放置,就是 TCP 的特征之一,面向字节流。


TCP 的 超时重传机制

主机 A 发送数据给 B 之后,可能因为网络拥堵等原因,数据无法到达主机B。如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答,就会进行重发。

例如,主机 A 和主机 B 通信:
A to B:发送数据(1~1000)
// 一段时间没有收到 B 的确认应答,A 判定丢包,开始重传
A to B:发送数据(1~1000)
B to A:确认应答(收到了,下一个从 1001 开始发)

因此主机 B 会收到很多重复数据,那么 TCP 协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候报头里的序列号,就可以很容易实现去重。

超时的时间如何确定呢?

  • 最理想的情况下,找到一个最小的时间,保证“确认应答一定能在这个时间内返回”。但是这个时间的长短,随着网络环境的不同,是有差异的:如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包。

TCP 为了保证无论在任何环境下都能比较高性能的通信,此会动态计算最大超时时间。

  • Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。如果重发一次之后,仍然得不到应答,等待 2500ms 后再进行重传。如果仍然得不到应答,等待 4500ms 进行重传。依次类推, 以指数形式递增。累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接。

TCP 的 三次握手

整个 TCP 三次握手建立连接,传递信息,四次挥手断开连接的过程,包括应用层的建立。我们需要关注的是传输和两端的状态变化,流程图如下:
在这里插入图片描述

三次握手的过程,由双方的 OS 系统中的 TCP 层自主完成。

客户端:connect,触发连接,等待完成
服务器:accept,等待建立完成,获取连接

为什么握手是三次?

  • 没有明显的设计漏洞(如果是 2 次会造成客户端零成本连接,服务器可能收到连接攻击而崩溃),一旦建立连接出现异常,成本嫁接到 client 端,server 端成本较低(因为最后一次发送可能丢失,如果偶数次握手,server 是不确定丢包与否即建立成功与否的)。

  • 可以验证双方通信信道的通畅情况,三次握手是验证全双工通信信道通畅的最小成本。

三次握手除了确认信道畅通建立连接,还有什么用?

  • 通过 16 位窗口大小进行流量控制。首先我们要知道,TCP 是面向连接的,处于正常通信之前,三次握手是不携带有效数据的,也就是说,两端第一次传输数据不是第一次通信。
  • 在三次握手阶段,首先,客户端在发起连接请求时,除了 SYN 标志位被置 1,客户端也一定会将自己的 16 位窗口大小通告给服务器,以便服务器知道该客户端的承载能力。
  • 其次,服务器响应 SYN + ACK 标志位被置 1,服务器的 16 位窗口大小也会被填上。这样双方的承载能力就都被对方知晓了,不会在第一次报文传输的时候出现流量异常导致的一系列问题。

服务器端状态转化:

  • [CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态,等待客户端连接

  • [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送 SYN 确认报文

  • [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了

  • [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close),服务器会收到结束报文段,服务器返回确认报文段并进入 CLOSE_WAIT

  • [CLOSE_WAIT -> LAST_ACK] 进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据),当服务器真正调用 close 关闭连接时,会向客户端发送 FIN,此时服务器进入 LAST_ACK 状态,等待最后一个 ACK 到来(这个ACK是客户端确认收到了 FIN)

  • [LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK,彻底关闭连接

客户端状态转化:

  • [CLOSED -> SYN_SENT] 客户端调用 connect,发送同步报文段

  • [SYN_SENT -> ESTABLISHED] connect 调用成功,则进入 ESTABLISHED 状态,开始读写数据

  • [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用 close 时,向服务器发送结束报文段,同时进入 FIN_WAIT_1

  • [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段

  • [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段,进入 TIME_WAIT,并发出 LAST_ACK

  • [TIME_WAIT -> CLOSED] 客户端要等待一个 2 MSL(Max Segment Life,报文最大生存时间)的时间, 才会进入 CLOSED 状态。

TCP 的 四次挥手

综合上述的描述,对四次挥手进一步做些解释。

四次挥手对于主动断开的一方 进行最后一次确认后,要进入 TIME_WAIT 状态,这是一个临时性状态,持续一会就没了。

为什么挥手是四次?

  • 建立连接后,两端就是同等地位了,一方断开连接都需要另一方确认。

TIME_WAIT 状态的细节:

  • 当主动断开的一方进入 TIME_WAIT 状态时,连接确实就断开了,但底层这个连接还没有被彻底关掉,相应的端口号还在被 TIME_WAIT 状态占用,在被占用的这段时间里,这个端口号就是不能使用的。

  • 如果是 server 端发起断开连接并处于 TIME_WAIT 状态,而 server 端经不起等待或者更换端口时,就可以调用下面的接口,对套接字进行相应的选项设置,无视其TIME_WAIT 状态,重新使原端口可以被有效使用。

setsockopt 函数:设置套接字选项,解决 TIME_WAIT 状态引起的 bind 失败

 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
                void *optval, socklen_t *optlen);  
int setsockopt(int sockfd, int level, int optname,
                const void *optval, socklen_t optlen);

参数 level:

  • 要设的属性在哪一层

参数 optname:

  • 要设的是哪一个属性

  • SO_REUSEADDR:无视网络中的 TIME_WAIT 状态。主动关闭连接的一方处于 TIME_WAIT 的时候,允许新的连接重新绑定与 TIME_WAIT 状态的连接有冲突的 IP + PORT(),并立即接收数据。

  • SO_REUSEPORT: SO_REUSEADDR 对完全相同的IP+PORT绑定(无论是具体的IP还是通配)仍然出现Address already in use的错误,使用SO_REUSEPORT选项可以避免此错误。

参数 optval:

  • 属性的值设成多少

参数 optlenl:

  • 属性长度是多少

·

TIME_WAIT 状态有什么用?

  • 当退出端退出时,或有正在传输的信息并未到达对端,TIME_WAIT 的存在,就是为了让已退出端尚未完成传输的信息消散,目的是不影响对端 ACK 的序号不被先前信息影响,保证下次同个端口号发送的数据能够被正常响应。

  • 另一方面,是为了保证退出端的最后一次 ACK 被对端收到。


TCP 的 流量控制

之前提到过,接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做 流量控制(Flow Control)

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK端通知发送端。窗口大小字段越大, 说明网络的吞吐量越高。

  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。发送端接受到这个窗口之后,就会减慢自己的发送速度。

  • 如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个 窗口探测 数据段,使接收端把窗口大小告诉发送端。

  • 窗口探测:发送方定期发出一个 TCP 报头,接收方必须 对所有请求进行应答(TCP 的协议内容),于是返回应答中的 16位 窗口字段就可以让发送方知道,什么时候可以继续发送有效载荷。

在TCP首部中,有一个 16 位窗 口字段,用来存放窗口大小信息。

那么问题来了,16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么? 

实际上, TCP 首部 40 字节选项中还包含了一个 窗口扩大因子 M,实际 窗口大小是 窗口字段的值左移 M 位


TCP 的 滑动窗口

在这里插入图片描述
对应确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。如此串行工作,性能较差,尤其在数据往返的时间较长的时候更甚。

一发一收的方式性能较低,只要一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个段)。

  • 发送前四个段的时候,不需要等待任何 ACK,直接发送。

  • 收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。

  • 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉。

  • 窗口越大, 则网络的吞吐率就越高。

一般情况

在这里插入图片描述

  1. 滑动窗口往左边部分,是已经发送已经确认过的无效数据,可以被覆盖

  2. 滑动窗口部分,暂时不用等待收到应答,可以直接发送。其大小和对方的接受能力有关,即应答报文中的窗口大小。

  3. 滑动窗口往右边部分,是尚未发送的数据区域。

如何理解滑动窗口?

在这里插入图片描述

  • 所谓面向字节流的 TCP,他的接收和发送缓冲区都可以理解成一个个 char 类型的 数组。

  • 而滑动窗口在这个数组中的“滑动”,实际是依赖两个指针(这里做 winstart,winend)划定范围,通过 ++ 运算符完成的。

一些规则

  1. “窗口” 只会向右 “滑动”,左边是已经确认过的报文

  2. 一直向右移的规则并不会造成越界,因为发送缓冲区的结构是环状的。

  3. 滑动窗口的大小是浮动的,不是固定大小,而其 变大、变小、变 0 是根据对方给本端响应报头中 窗口大小 来调节的。

     窗口变大通过 winend+=xxx 来完成
     窗口变小 winend-=xxx 来完成
    
  4. 滑动窗口大小的更新,更具体来说,和对方发送的序列号 seq 有关,在应答也是按序到达的前提下:

    根据应答的 seq,winstart = seq;
    根据应答的 win,winend = winstart + win;
    
  5. 滑动窗口内部的报文可以直接发送,多个报文传输,肯定会发生丢失的问题。

    如果第一个丢失了
    		情况1:数据丢了,即使后面的收到了,winstart 也不会向后移动,只是等待发送方超时重传。
    		情况2:应答丢失,可以通过后面传来的响应报文确定,之前的一定接收到了。
    如果中间或最后的丢失了
    		随着 winstart 的右移都会转化成第一个丢失问题,按上述步骤处理。
    
  6. 数据要支持重传,就必须被保存起来,保存的位置就是滑动窗口


TCP 的 拥塞控制:慢启动机制 和 阈值

少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为 网络拥塞

当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降。

当网络拥塞出现大量丢包的情况:发送方不能一直超时重传也不能完全停发。总体策略是,保证网络拥塞不能加重,再网络拥塞有起色的情况下,尽快恢复网络通信。

慢启动 机制,先发少量的数据探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

此处引入一个概念程为 拥塞窗口

  • 发送开始的时候,定义拥塞窗口大小为 1

  • 每次收到一个 ACK 应答,拥塞窗口加 1

    只是这样的话,拥塞窗口的大小肯定是指数级上升的,可实际不止如此。
    
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口

    滑动窗口 = min(对端主机的接受能力 win,网络的拥塞窗口)
    winstart = seq;
    winend = min(seq_win, 拥塞窗口);
    

正常来说拥塞窗口增长速度,是指数级别的。“慢启动” 只是指初使时慢,但是增长速度非常快。为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,需要 慢启动的 阈值 进行控制。

在这里插入图片描述

  • 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
  • 当 TCP 开始启动的时候,慢启动阈值等于窗口最大值
  • 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回 1

拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。


TCP 的 延迟应答

如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。

假设接收端缓冲区为 1M,一次收到了500K的数据,
如果立刻应答,返回的窗口就是500K。
但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了

在上述情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候返回的窗口大小就是 1M。

在这里插入图片描述

窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。如何选择延迟应答的时机呢?

选择延迟应答的时机,也有不同的方案:

  • 数量限制:每隔 N 个包就应答一次
  • 时间限制:超过最大延迟时间就应答一次

具体的数量和超时时间,依操作系统不同也有差异,一般 N 取 2,超时时间取 200ms。


TCP 的 捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。
意味着客户端给服务器说了 “你好吗”,服务器也会给客户端回一个 “我很好”。那么这个时候 ACK 就可以和服务器回应的 “我很好” 一起回给客户端。

在这里插入图片描述


TCP 的 异常情况

机器重启 / 进程终止:机器重启 / 进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没有什么区别。

机器掉电 / 网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行 reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。

另外,应用层的某些协议,也有一些这样的检测机制

例如 HTTP 长连接中,也会定期检测对方的状态,例如QQ,。
在 QQ 断线之后,也会定期尝试重新连接。

小结

TCP 这么复杂,是因为要保证 可靠性,同时又尽可能的提高性能。

可靠性
校验和
序列号(按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能:
滑动窗口
快速重传
延迟应答
捎带应答
其他:
定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

另,基于TCP应用层协议有如下这些:

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP
    当然, 也包括我们自己写 TCP 程序时自定义的应用层协议

🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值