TCP 传输控制协议(二、连接管理)

TCP(传输控制协议) TCP(传输控制协议) 传输控制协议(Transmission Control Protocol,TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。 在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。 一、TCP/IP协议族 TCP/IP(TCP/IP Protocol Suit 阅读详情

TCP 传输控制协议(二、连接管理)

引言

TCP是一种面向连接的单播协议。在数据交换之前,双方必须先建立连接。TCP的服务模型是一个字节流。TCP会修复下层(如IP层,链路层等)产生的数据传输问题,对于数据交换来说,大致有三个方面的问题:丢包、重复和错误。为了解决这些问题,TCP引入了连接的概念。***可以说,UDP和TCP本质上最大的区别在于TCP需要维护连接。TCP由于要维护连接所以TCP是有状态的,相比于无状态的UDP,TCP在妥善处理多种TCP状态问题时需要面对大量的细节问题,比如连接的建立、终止和重启。这一章我们就来一起讨论关于TCP的连接管理。

TCP连接的建立与终止

一个TCP连接是有一个4元组构成的,简单来说就是一组IP和一组端口号。一个TCP连接通常分为三个阶段:启动、数据传输和退出。下面我们就对这三个阶段一步步讨论。

Imgur

​ 图 1

在图1中的时间轴描绘了一个连接建立过程中的相关事宜。

连接建立

正常的连接建立流程(三次握手)

首先我们先讨论一个TCP连接建立的过程:

  1. 客户端发送一个SYN报文段,并指明自己想要连接的端口和它的客户端初始序列号(记为ISN©)。通常客户端还会借此发送一个或多个选项。客户端发送的这个SYN报文段称作段1。这个时候客户端进入SYN-SENT状态

  2. 服务端收到SYN报文段后,进入LISTEN状态。服务端也发送自己的SYN报文段作为相应,并包含了它的 初始序列号(ISN(s))。该段称为段2。为了确认客户的的SYN,服务器将其包含的ISN©数值加1后作为返回的ACK数值,然后进入SYN-RCVD状态。因此,每送一个SYN,序列号就会自动加1。这样如果出现丢失的情况,该SYN段将会重传。

  3. 客户端收到报文后,客户端进入ESTABLISHED状态,这个时候对于客户端来说已经是可以交换数据的状态了。为了确认服务器的SYN,客户的将ISN(s)的数值加1后最为返回的ACK值,这称为段3。

    • 服务端收到后,进入ESTABLISHED状态,这个时候双方就进入正式的数据交换过程。
      TCP A                                                TCP B

  1.  CLOSED                                               LISTEN

  2.  SYN-SENT    --> <SEQ=100><CTL=SYN>               --> SYN-RECEIVED

  3.  ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK>  <-- SYN-RECEIVED

  4.  ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK>       --> ESTABLISHED

  5.  ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED

          Basic 3-Way Handshake for Connection Synchronization
 						在[rfc793]中对于三次握手的图示

通过上面这三个报文段就能够正常建立一个TCP连接,也称为三次握手。三次握手的目的不仅在于让通信双方连接一个连接正在建立,还在于利用数据报的选项来交换特殊信息,交换初始序列号ISN。一般来说,三次握手是首个发送SYN的一方被认为主动打开一个连接,我们一般称之为客户端,而被动打开连接的一方我们称之为服务端。但是有一种特殊情况是两个端点同时发起连接,下面会讨论这种特殊的情况。

同时打开 Simultaneous initiation

两个端点同时给对方发送主动建立连接的情况不多见,但是在特定的场景下是有机会出现的。前提是两个端点都有对方的ip和端口,就有可能发生这种情况,对于同时打开,需要四个报文段。

      TCP A                                            TCP B

  1.  CLOSED                                           CLOSED

  2.  SYN-SENT     --> <SEQ=100><CTL=SYN>              ...

  3.  SYN-RECEIVED <-- <SEQ=300><CTL=SYN>              <-- SYN-SENT

  4.               ... <SEQ=100><CTL=SYN>              --> SYN-RECEIVED

  5.  SYN-RECEIVED --> <SEQ=100><ACK=301><CTL=SYN,ACK> ...

  6.  ESTABLISHED  <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

  7.               ... <SEQ=101><ACK=301><CTL=ACK>     --> ESTABLISHED

                Simultaneous Connection Synchronization

从上面的图可以看出,同时打开每个TCP都是从CLOSED 到 SYN-SENT 再到 SYN-RECEIVED 到已确立的。

这里说一下我个人关于三次握手的一些想法。大家对这个流程想必都不陌生,但是我感觉很多人在描述这个过程的时候并没有说出关键的信息,就是三次握手每次是为了什么。首先握手的目的就是双方确认对方的收发能力,对于两端来说要确认的东西是一样的,确认对方能接受自己的信息,能给自己发送信息。

  • 客户端给服务端发消息的时候,这时候服务端知道客户端有发送消息的能力。

  • 服务端给客户端发送消息的时候,客户端就知道服务端有接受消息和发送信息的能力。

  • 客户端再次给服务端发送消息,这时候服务端就知道客户端有接受信息的能力(通过ACK知道消息是应答而不是重复发)。

不管是哪一端,收到对方的消息就是代表对面有正常发送消息的能力,收到对方的ACK = 己方发送的SEQ + 1就证明对方是有正常的接收能力。

这也很好解释为什么不是四次握手,因为三次握手就已经足够让两端都确认对方有正常的收发消息的能力。

当然在三次握手的过程中还处理了很多别的细节问题,不过在我看来,三次握手本质上要做的事情只有一件,就是确认对方的收发能力。

再来看同时打开为什么是四次,其实就是接收到对方正确的ACK回复就证明对方有正常的收发能力。

这里举一个例子,如果没有三次握手有可能发生什么情况:

TCP A发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达TCP B。本来这是一个早已失效的报文段。但TCP B收到此失效的连接请求报文段后,就误认为是TCP A再次发出的一个新的连接请求。于是就向TCP A发出确认报文段,同意建立连接。

假设不采用“三次握手”,那么只要TCP B发出确认,新的连接就建立了。由于是TCP A发送的请求报文段滞留后才到达服务端,而现在TCP A并没有发出新的建立连接的请求,因此不会理睬TCP B的确认,也不会向TCP B发送数据(此时的 TCP A 对于 TCP B 其实是没有接收能力的,因为接收到什么都是扔掉的**)。但TCP B却以为新的连接已经建立,并一直等待TCP A发来数据。这样,TCP B的很多资源就白白浪费掉了。

TCP 快启TCP Fast Open

TCP 快启策略使用存储在客户端的 TFO Cookie 与服务端快速建立连接。

TCP 连接的客户端向服务端发送 SYN 消息时会携带快启选项,服务端会生成一个 Cookie 并将其发送至客户端,客户端会缓存该 Cookie,当其与服务端重新建立连接时,它会使用存储的 Cookie 直接建立 TCP 连接,服务端验证 Cookie 后会向客户端发送 SYN 和 ACK 并开始传输数据,这也就能减少通信的次数。

  1. 客户端发送SYN包,包尾加一个FOC请求,只有4个字节。

  2. 服务端受到FOC请求,验证后根据来源ip地址声称cookie(8个字节),将这个COOKIE加载SYN+ACK包的末尾发送回去。

  3. 客户端缓存住获取到的Cookie 可以给下一次使用。

  4. 下一次请求开始,客户端发送SYN包,这时候后面带上缓存的COOKIE,然后就是正式发送的数据。

  5. 服务器端验证COOKIE正确,将数据交给上层应用处理得到相应结果,然后在发送SYN+ACK时,不再等待客户端的ACK确认,即开始发送相应数据。

由于TFO存在诸如TFOcookie多长时间后删除,谁来维护和删除和一些安全隐患等问题所以没有大面积推广,这个不做过多讨论。之后有机会再单独和大家聊一下这个东西。

关闭连接

正常的关闭连接流程

我们还是先来看正常的关闭连接流程,双方不管哪一方都可以主动发起关闭操作。TCP协议规定通过发送FIN段来发起关闭操作。我们先来看一下完整的流程:

  1. TCP A发送一个FIN段指明接受者希望看到的自己当前的序列号K,FIN段还包含一个ACK段用于确认对方最近一次发来的数据,并且停止发送数据,此时TCP A进入FIN-WAIT-1状态。(FIN报文不管是否携带数据,都需要消耗一个序列号)

  2. TCP B将序列号K的数值加1作为ACK的值,表明已经接受到FIN,进入了CLOSE-WAIT状态。这个时候上层的应用程序会被告知连接的另一端已经提出了关闭的请求,接下来就是应用程序自己的操作了。(这个时候TCP A向TCP B的方向就释放了,但是TCP B向TCP A发送数据还是可以的,这个状态会持续整个CLOSE-WAIT状态)

    1.这时候TCP A接收到到来自TCP B的应答报文后,进入FIN-WAIT-2状态,等待TCP B发送连接释放报文,在这个过程中是要处理来自TCP B发过来的消息的。

  3. TCP B发送完最后的数据之后,就发送自己的FIN报文,该报文的序列号为L,然后进入进入了LAST-ACK状态。

    • 这时候TCP A接收到到来自TCP B的连接释放报文后,状态进入TIME-WAIT,并不是马上释放,等待2MSL之后才会进入CLOSED状态。
  4. TCB A为了完成关闭连接,最后发送的报文段还包含一个ACK = L + 1用于确认上一个FIN。

    • TCP B只要收到了TCB A发出的确认,立即进入CLOSED状态

值得注意的是,如果出现FIN丢失,那么发送方将重新传输知道接收到一个ACK确认为止。

      TCP A                                                TCP B

  1.  ESTABLISHED                                          ESTABLISHED

  2.  (Close)
      FIN-WAIT-1  --> <SEQ=100><ACK=300><CTL=FIN,ACK>  --> CLOSE-WAIT

  3.  FIN-WAIT-2  <-- <SEQ=300><ACK=101><CTL=ACK>      <-- CLOSE-WAIT

  4.                                                       (Close)
      TIME-WAIT   <-- <SEQ=300><ACK=101><CTL=FIN,ACK>  <-- LAST-ACK

  5.  TIME-WAIT   --> <SEQ=101><ACK=301><CTL=ACK>      --> CLOSED

  6.  (2 MSL)
      CLOSED

                         Normal Close Sequence

和启动不同,关闭一个TCP连接需要4个报文段。关于TCP连接关闭,有两个问题值得拿出来讨论的。

  1. 为什么客户端最后要等待一个2MSL的时间才进入CLOSED?

    • 原因是保证客户端能处理“服务端没有正常接收到客户端最后一个ACK报文”这种情况,如果客户端最后发送给服务端的应答报文丢失,那么服务端就是重新发送FIN+ACK报文请求断开连接,在正常情况客户端会在2MSL内收到这条请求,如果没收到就当服务端已经正常关闭。如果在2MSL内收到了服务器的重新请求,客户端就会重新应答,然后重新启动一个2MSL的计时器。
  2. 为什么挥手要四次?

    • 我们仔细观察第二三次挥手,其实就是把第二次的握手一分为二了。我们回忆下建立连接的时候,服务器是在LISTEN的状态下,收到请求连接的SYN后,将ACK和SYN放在第二次握手的报文中发送给客户端的。但是在关闭连接的时候,服务器确认的ACK和服务器要断开连接的FIN是在两个不同的报文中处理的,所以就多了一次连接。那么为什么第二三次要拆开,原因是服务端收到关闭连接请求后,是要先处理完在等待发送的数据,然后才关闭SOCKET,中间这个时间可长可短,对于客户端来说可能会误以为报文丢失,引起不必要的重传,而且这样客户端本可以切换状态告诉上层应用,也变得要一直等待服务端响应。
同时关闭

和开启一样,关闭也有可能出现同时关闭的情况。下面先来看一下同时关闭的时序图。

同时关闭就相对比价简单,同时关闭和正常关闭需要交换的报文段数量是相同的,两者的却别在于报文段序列是交叉还是顺序的。

半关闭

TCP半关闭其实是一些应用需要此项功能,本身这个功能并不是特别常见。这个需求顾名思义就是通信双方一方完成了数据发送的工作,然后发送一个FIN给对方,但是仍然希望对方在发送FIN给我之前我能收到来自对方的数据。在伯克利套接字API里面,正常关闭的API是close(),半关闭就是shutdown()。下面这里有瓣关闭的流程图,可以看到,客户端发起关闭的流程和正常关闭是一样的,中间的数据段传输过程是可以传输任意数量的数据段,知道另一方发送完数据,发送FIN给客户端之然后收到客户端的确认之后,整个连接才完全关闭。

初始序列号

根据之前的内容,我们知道任何拥有合适的IP地址、端口号、符合逻辑的序列号以及正确的校验和的报文段都将被对方接收,这个时候就有一个问题,TCP报文段在金国网络略有后可能会存在延迟抵达和排序混乱的情况。为了解决这种问题,初始序列号就不能是简单的0或1,初始序列号的选择就变得非常关键了。

首先如果初始序列号是1,我们来看下会发生什么情况。C端和S端建立了连接,这个时候C端给S端发送了10个包,但是由于网络问题,这10个包在网络中滞留了。这个时候C端用相同的端口号和S端发起连接,然后发了5个包。接着,之前在网络中滞留的10个包到达了S端,由于IP地址,端口号,序列号这些统统都一样,所以服务端会当作合法包接收,然后回一个回应包给C端,这个包的确认号是10,但是对于C端只发了5个包,所以一切就乱套了。、

[rfc0793]给出的建议是The generator is bound to a (possibly fictitious) 32 bit clock whose low order bit is incremented roughly every 4 microseconds.ISN的生成器绑定到一个32位的虚拟时钟上,每4微妙递增一次。对于同一个连接的两个实例而言,新的序列号不能出现重叠的情况。其实从这里就可以看出,TCP还是挺脆弱的,只要知道IP地址,端口号,序列号就可以伪造出合法的报文段。对于这种潜在的安全问题,要么是然序列号难以猜测,要么就是加密,加密我们之后会另行讨论。

TCP选项

前面我们说到,TCP头部包含了多个选项,下面我们就来讨论一下几个比较重要的选项。

MSS 最大段大小选项

最大段大小指的是TCP协议锁允许从对方接收到的最大报文段。MSS指的是TCP数据报的长度儿不包括TCP头部和其他TCP选项。MSS是在SYN报文中指定的,如果没有指定,那么默认大小是536字节。这里要明确一点,MSS不是双方协商的结果,是一个单方面的限定数值。

SACK

前面在聊滑动窗口的时候,提到过累计ACK确认,由于接收到数据是无序的,所以序列号也是无序的,这就会导致数据队列中***空洞***的出现。按照目前TCP的确认系统不是特别的好处理这种不连续确认的状况,目前是只有低于ACK number的包都被接收才进行ACK,out-of-order的片段只能是等待,这个时间窗口是无法向右移动的。为了解决这个问题,就引入了SACK(Selective Acknowledgment 选择性确认)。说简单点就是用于描述乱序数据,一般是接收方告诉发送方***空洞***是哪些。SACK的标准描述文件是[rfc2018]。

SACK作为一个TCP选项,想要使用必须是通信双方都同意使用,Sack-Permitted Option选项在SYN中携带。

       TCP Sack-Permitted Option:
       +---------+---------+
       | Kind=4  | Length=2|
       +---------+---------+

在两个端点都支持SACK时,检测到数据流中丢失数据包的对等方可以将此信息通知发送方。

       TCP SACK Option:
       Length: Variable

                         +--------+--------+
                         | Kind=5 | Length |
       +--------+--------+--------+--------+
       |      Left Edge of 1st Block       |
       +--------+--------+--------+--------+
       |      Right Edge of 1st Block      |
       +--------+--------+--------+--------+
       |                                   |
       /            . . .                  /
       |                                   |
       +--------+--------+--------+--------+
       |      Left Edge of nth Block       |
       +--------+--------+--------+--------+
       |      Right Edge of nth Block      |
       +--------+--------+--------+--------+
时间戳 防回绕序号

TCP最早是在[RFC1323]中引入的timestamp选项,并在[RFC7323]中进行更新。时间戳的作用一个有两个:

  1. 估算一条TCP连接的往返时间(round-trip-time,RTT),用于设置重传超时。
  2. 防止过期报文干扰正常通信 PAWS。

Timestamp作为一个TCP选项,在TCP首部占10个字节(8个字节用于保存两个时间戳,另外两个数值用于指明选项的数值与长度),具体如下图。

      TCP Timestamps Option (TSopt):

         Kind: 8

         Length: 10 bytes

          +-------+-------+---------------------+---------------------+
          |Kind=8 |  10   |   TS Value (TSval)  |TS Echo Reply (TSecr)|
          +-------+-------+---------------------+---------------------+
              1       1              4                     4

选项的核心数据是两个 32-bit 的时间戳字段.TSval 表示发送端发出该报文时的本地时间戳, 而 TSecr 则负责回放(Echo) 最近一次收到的对端报文中的 TSval 的值。**TSval **和 TSecr 在数值上并没有绝对的大小关系。TSval 是以本地的时钟为基准的, 而 TSecr 则是以对端的时钟为基准的。

以下是一个组典型的时间戳交互过程:

             TCP  A                                     TCP B

                             <A,TSval=1,TSecr=120> ----->

                  <---- <ACK(A),TSval=127,TSecr=1>

                             <B,TSval=5,TSecr=127> ----->

                  <---- <ACK(B),TSval=131,TSecr=5>

启用 Timestamp 选项需要经过双方的协商,协商在三次握手时完成,如果协商成功,则在后续的报文中, 除了 RST 之外的所有报文均必须包含 Timestamp 选项。

TSval是以本地时间为基准,但是不一定要与真实时间相同,我们只需要保证TSval不要太快或者太慢就行。

  1. 不能太慢时因为如果tick的时间过长会影响RTT的计算结果,例如10s tick一定下,对于往返时间小于10s的连接来说,算出来的RTT甚至有可能是0。
  2. 不能太快是因为PAWS,因为TCP协议规定最大的MSL只有255s,那么一次时钟的循环必须大于这个值,32-bit的时钟tick必须大于59ns。否则就无法区分两个时间戳释放经历了时间戳回绕(这个下面会提到)。RFC 规定虚拟时钟的频率为每 1ms 到 1s 一个 tick。 按照 1ms 计算,32-bit 的时间戳回绕一次需要 24.8 天。

TSecr一般来说填入对端上一个报文的 TSval 就行,但是下面三种特殊情景需要注意一下:

  1. Delay ACK,这个选项的本质是为了减少网络中的pure ACK,其实就是前面说道的延时应答机制,等报文积累到一定程度再进行应答(上一章提到过这个设计)。开启了Delay ACK后,当启用了Delay ACK后接收端收到多个报文的时候,就会出现多个时间,这个时候我们用最早的报文的 TSval,只有这样算出来的RTT才准确。

  2. 空洞 发送端发送了多个报文,但是由于网络原因出现了空洞,意味着可能发生了网络阻塞,这个时候我们最好是可以让接收方echo早一点的TSval,而不是序号最大的报文的TSval,这样的发送方计算出的RTT会偏大,发送报文就更保守,有利于减小阻塞。

  3. 空洞补上 这里有两种可能:

    1. 乱序的报文来迟了
    2. 收到的是重传的报文

    不管是哪种,都要Echo当前报文的TSval,这样才能反映真实的网络情况。

算法

针对上面说的情景,在RFC[1323]中有设计出一套解决的算法可以处理上面问题的同时也能兼容正常场景。

  1. 连接状态增加两个32位的变量 TS.Recent 和 Last.ACK.sent。TS.Recnet保存下一个填入TSecr的时间戳。Last.ACK.sent保存上一个有序报文的ACK。在不启用delay ACK的时候,Last.ACK.sent 和 RCV.NXT是保持一致的。当启用delay ACK的时候,每收到一个有序报文,RCV.NXT就会向后推进,Last.ACK.sent 只会在真正 ACK 报文发送过后才更新。
  2. 如果收到的报文时间戳大于TS.Recent并且报文没有造成空洞,则立刻更新TS.Recent的值
if SEG.TSval >= TS.Recent and SEG.SEQ <= Last.ACK.sent
then 
	TS.Recnet = SEG.TSval

来看一下RFC[1323]中,针对不同情况给出的官方例子。

  • 接收端启用了delay-ACK, 报文顺序按照A -> B -> C 到
                                                TS.Recent   Last.ACK.sent  RCV.NXT
              <A, TSval=1> ------------------->                             
                                                    1            A          A->B
              <B, TSval=2> ------------------->
                                                    1            A          B->C
              <C, TSval=3> -------------------> 
                                                    1            A          C->D
                       <---- <ACK(C), TSecr=1>                   
              (etc.)                                1           A->D         D

这个例子可以看到启用了delay-ACK之后,TS.Recent的值一直是收到的一个包的TSval = 1,Last.ACK.sent真正 ACK 报文发送过后才更新,RCV.NXT却是马上更新的。

  • 未启用delay-ACK,报文乱序
                                                TS.Recent   Last.ACK.sent  RCV.NXT
              <A, TSval=1> ------------------->
                                                    1           A           A->B 
                       <---- <ACK(A), TSecr=1>
                                                    1          A->B          B
              <C, TSval=3> ------------------->
                                                    1           B            B                         
                       <---- <ACK(A), TSecr=1>
                                                    1           B            B
              <B, TSval=2> ------------------->
                                                   1->2         B           B->D           
                       <---- <ACK(C), TSecr=2>
                                                    2          B->D          D
              <E, TSval=5> ------------------->
                                                    2            D           D
                       <---- <ACK(C), TSecr=2>
                                                    2            D           D
              <D, TSval=4> ------------------->
                                                   2->4          D          D->F       
                       <---- <ACK(E), TSecr=4>
                                                    4           D->F         F
              (etc.)

这个例子看C, D就行了,报文 C 到达后, 由于造成了接收端序号空洞,因此不会更新 TS.Recent, 报文 B 到达, 填上了空洞,因此 TS.Recent 更新,根据TS.Recent更新原则,这个时候需要用B的TSval更新到TS.Resent,后面D包到的时候也是同理。Last.ACK.sent 和 RCV.NXT都是在没有空洞之后才更新。

PAWS Protection Against Wrapped Sequences 防止序号回绕

先把PAWS的原理说给大家,PAWS本质就是利用***时间戳是单调递增***的这一数学特性来判断报文是否过期。

TSval是发送端发送报文的时间戳,因此如果SEG.TSval < TS.Recent,则说明这个报文时老旧的过期报文,可以丢弃。因为前面说过,TS.Recent必须是没有空洞的时候才会更新。假设报文SEG1在网络中滞留,然后发送端重传了SEG2。这个时候接收端先收到了SEG2,更新TS.Recent = SEG2.TSval,这个时候SEG1到了,因为SEG1.TSval < SEG2.TSval,所以SEG1.TSval < TS.Resent,因此根据规则SEG1被丢弃。

TCP高速连接的场景中,32位的序号有可能在短时间内用完,这时候如果没有时间戳就有可能会混淆两个序号相同的包。TCP Timestamp本质就是序列号的扩充。

有一个非常特殊的情况是,由于32位的时间戳回绕一次的时间是24.8天,所以如果一条TCP连接在24.8天之内都没有收到另一端的数据,这个时候TS.Recnet就会视作失效,接下来的第一个报文是不能用PAWS校验的,必须从第二个开始使用。但是这种情况比较罕见。

TCP Timestamp虽然有很多好处,但是有一个很明显的弊端就是,太长了。所以并不是所有的TCP都会使用Timestamp选项,例如windows就是默认关闭的,我们熟悉的Linux就是默认打开的。

用户超时选项

用户超时(User TimeOut, UTO)指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间。[RFC0793]中UTO是TCP协议本地配置的一个参数。用户超时选项允许TCP通信方将自己的UTP告诉对方,方便TCP接收方做出调整。但是UTO时建议性的,因为接收方不是一定要遵从发送方发过来的数值。

TCP状态转换

TCP状态转换图

前面我们提到TCP是有状态的,下面我们就来看看TCP在各种状态之间的切换,首先来看看TCP的状态图。这里直接放出[rfc0793]里面给出的状态图。

                                           Transmission Control Protocol
                                                Functional Specification




                              +---------+ ---------\      active OPEN
                              |  CLOSED |            \    -----------
                              +---------+<---------\   \   create TCB
                                |     ^              \   \  snd SYN
                   passive OPEN |     |   CLOSE        \   \
                   ------------ |     | ----------       \   \
                    create TCB  |     | delete TCB         \   \
                                V     |                      \   \
                              +---------+            CLOSE    |    \
                              |  LISTEN |          ---------- |     |
                              +---------+          delete TCB |     |
                   rcv SYN      |     |     SEND              |     |
                  -----------   |     |    -------            |     V
 +---------+      snd SYN,ACK  /       \   snd SYN          +---------+
 |         |<-----------------           ------------------>|         |
 |   SYN   |                    rcv SYN                     |   SYN   |
 |   RCVD  |<-----------------------------------------------|   SENT  |
 |         |                    snd ACK                     |         |
 |         |------------------           -------------------|         |
 +---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+
   |           --------------   |     |   -----------
   |                  x         |     |     snd ACK
   |                            V     V
   |  CLOSE                   +---------+
   | -------                  |  ESTAB  |
   | snd FIN                  +---------+
   |                   CLOSE    |     |    rcv FIN
   V                  -------   |     |    -------
 +---------+          snd FIN  /       \   snd ACK          +---------+
 |  FIN    |<-----------------           ------------------>|  CLOSE  |
 | WAIT-1  |------------------                              |   WAIT  |
 +---------+          rcv FIN  \                            +---------+
   | rcv ACK of FIN   -------   |                            CLOSE  |
   | --------------   snd ACK   |                           ------- |
   V        x                   V                           snd FIN V
 +---------+                  +---------+                   +---------+
 |FINWAIT-2|                  | CLOSING |                   | LAST-ACK|
 +---------+                  +---------+                   +---------+
   |                rcv ACK of FIN |                 rcv ACK of FIN |
   |  	rcv FIN       -------------- |    Timeout=2MSL -------------- |
   |  -------              x       V    ------------        x       V
    \ snd ACK                 +---------+delete TCB         +---------+
     ------------------------>|TIME WAIT|------------------>| CLOSED  |
                              +---------+                   +---------+

                      TCP Connection State Diagram
                               Figure 6.

这个状态转换图画出了完整的TCP有限状态机的所有的状态转换。这个图将CLOSED状态用作开始状态点和终止状态点(这个其实并不能算作一个“官方”的状态)。

我们来大致拆解一下这个图。

  • 建立连接部分
    • CLOSED → SYN_SEND → ESTAB 这个客户端正常发起连接的流程。
    • CLOSED → SYN_SEND → SYN_RCVD → ESTAB 这个是同时打开的流程。和上面的区别就在于 SYN_SEND后在收到自己的SYN + ACK之前先收到了对方的SYN报文。
    • CLOSED → LISTEN → SYN_RCVD → ESTAB 这个是服务器正常监听的流程。

这里有两点是值得注意的。

一、 从LISTEN到SYN_SENT的状态转换是合法的,但是却不被伯克利套接字支持,因为很少有这种情况出现。FTP中是会出现服务端主动对客户端发连接。

二、从SYN_RCVD回到LISTEN的状态只有在SYN_RCVD状态是由LISTEN状态转换而来(并非从SYN_SEND而来)才是正确的。可能的场景是:我们执行一个被动打开操作,接收一个SYN,发送一个带有ACK的SYN进入SYN_RCVD,然后收到一个重置消息,这个时候就返回LISTEN状态。

  • 断开连接部分
    • CLOSE_WAIT → LACK_ACK → CLOSE 是比较典型的“服务端”关闭状态流程。
    • FIN_WAIT_1 → FIN_WAIT2 → TIME_WAIT → CLOSE 这个是典型的“客户端”主动关闭状态流程。
    • FIN_WAIT_1 → CLOSING → TIME_WAIT → CLOSE 这个是同步关闭的状态流程。

TIME_WAIT 状态

time_wait状态只有主动关闭的一方才有,time_wait状态是主动发起关闭一方收到对方的FIN包之后进入的状态,持续时间为2MSL,如果期间重新收到了对方的FIN包,则重发ACK包并重置计时器重新计时,直到2MSL后收不到对方的消息,则进入CLOSE状态。

关于time_wait我们一个个来说,首先是MSL是什么?MSL是Maximum Segment Lifetime,报文最大生存时间,这个时间是报文在网络中可存活的最长时间,一旦报文在网络中存在的时间超过了这个时间,则会被丢弃。IP头中的TTL也是限制报文存活的条件,TTL是IP数据报可以经过的最大路由数量。MSL和TTL一个是时间长度另一个数字。这里讲一个规则:MSL应该要大于等于TTL消耗为0的时间,因为这样才可以保证报文自然消亡。

说完了MSL的概念,我们来说一下TIME_WAIT状态为什么要出现,为什么不是主动关闭方收到对方的FIN包后发一个ACK包就直接关闭,还需要等待一个2MSL?为什么是2MSL,不是3MSL, 4MSL,8MSL?

首先我们来说一下,TCP四次挥手的时候,假设没有TIME_WAIT可能会遇到什么问题。

  1. 第一个原因在[rfc793]中有明确的描述,这里我直接上原文了。

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

简单来说就是要保证被动关闭方能收到最后的ACK包。

  1. 第二个原因是,当客户端发起关闭连接流程,在收到服务端的FIN包后发出对应的ACK包然后直接关闭,但是ACK包丢失了服务端在一定时间内没有收到客户端的ACK包重发了FIN包,这个时候客户端马上又发起连接,就会收到上一次服务端发过来的FIN包。虽然组成socket的四元组一样,但是却不是上一次的连接了。或者说有一个网络包在网络中堵住了,然后这个时候客户端和服务端在MSL内完成了关闭,然后又发起连接,这个时候就会有存在网络中的旧包干扰到新的连接。

总的来说TIME_WAIT最大的作用就是:1、最大程度保证正常关闭。2、防止旧包干扰。

好了,当我们确定需要TIME_WAIT之后,TIME_WAIT的时间要怎么定,又是一个值得探讨的问题。上面有两个原因,正常来说,就是看哪个需要消耗的时间长,我们取哪个就可以了。

ACK报文被B接收到。我们假设A发送了ACK报文后过了一段时间t之后B才收到该ACK,则有 0 < t <= MSL。因为A并不知道它发送出去的ACK要多久对方才能收到,所以A至少要维持MSL时长的TIME_WAIT状态才能保证它的ACK从网络中消失。同时处于LAST_ACK状态的B因为收到了ACK,所以它直接就进入了CLOSED状态,而不会向网络发送任何报文。所以晃眼一看,A只需要等待1个MSL就够了,但仔细想一下其实1个MSL是不行的,因为在B收到ACK前的一刹那,B可能因为没收到ACK而重传了一个FIN报文,这个FIN报文要从网络中消失最多还需要一个MSL时长,所以A还需要多等一个MSL。

TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。

这里个有两个点需要注意的:

  1. 上面的例子中的server重发FIN包并不是等待到超时才重发的,server 会根据 net.ipv4.tcp_orphan_retries 的配置定时重发,直至收到 ACK 或重试次数到达上限,再关闭该连接。至于多久之内发送,是根据RTO算出来的,这个结果是会远小于MSL的。
  2. 其实2MSL并不能完全避免所有极端的情况,但是可以避免大多数情况的问题,2MSL可以算是一个权衡过后的值。

重置报文段

前面介绍TCP头部的时候有提到过RST位字段。该字段如果被启用,那么这个报文就叫做“重置报文段”。通常只有收到严重错误的报文才会产生重置报文,重置报文通常会导致TCP连接快速拆卸。

不存在接口

先说一个比较简单的情况,如果TCP收到一个连接请求但是并没有相关进程对对应的端口进行监听时,就会产生重置报文。以前在讲UDP的时候其实也有类似的情况,UDP遇到这种情况就会产生一个ICMP目的不可达的消息。TCP就会用重置报文来代替。

终止一条连接

前面我们提到,如果想终止一条连接,我们可以发送一个FIN包来走四次挥手的流程。我们称这种情况较有序释放。因为FIN是在之前所有排队的数据都发送出去后才被发送出去的,通常是不会出现丢失数据的情况。我们也可以通过发送一个重置报文段替代FIN来终止一条连接。通过重置报文段终止的方式我们称为终止释放。

终止一条连接可以为应用程序提供两大特性:

  1. 任何排队的数据都将被抛弃,一个重置报文段会被马上发出。
  2. 重置报文段的接收方会说明通信的另一端采用了终止的方式而非正常关闭。

API是会提供一种实现上述终止行为的的方式来取代正常的关闭操作。

时间等待错误

我们回到刚刚的四次挥手,假设前面当客户端进入TIME_WAIT状态的时候, 客户端的下一个序列号为 K, 而服务器的下一个序列号为 L, 最近到达的报文段是由服务器发送至客户端, 它使用的序列号为 L-100, 包含的序列号为 K-200, 客户端收到后认为的收到了旧报文段发送一个 ACK, 包含了最新的序列号 K, L, 服务器收到这个报文段后, 没有关于这条连接的任何信息, 因此发送重置报文段作为响应。这并不是服务器的问题,但是却会造成客户端过早进入CLOSED状态,所以有些系统规定当处于TIME_WAIT状态时不处理重置报文段。

小结

本章从简单的讨论了TCP从建立连接到交互数据到断开连接的基本过程,还有中间一些异常状态的产生和对应处理。简单讨论了一些TCP选项以为TCP为什么需要这些选项。下一章和大家聊一下TCP的超时与重传。

TCP长连接及连接管理 TCP 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。 TCP生命周期分为三个阶段: 建立连接 传输数据 关闭连接 根据使用方法不同,可认为TCP连接分为长连接和短连接。 在一次通信过程中,如果通讯过程结束就close掉连接,这就是短连接。 如果通讯结束后,保持tcp连接的状态不关闭,这就是长连接。 短连... 阅读详情

相关推荐

竞拍算法(Auction Algorithm)原理及工作过程分析

因一些项目工作需要对竞拍算法进行深入了解。但百度了大部分资料都未找到一篇文章能对此算法进行较为深入的介绍。在一番努力之下,终于找打了最初提出该算法的论文,本文内容主要结合该论文对竞拍算法进行分析。 竞拍算法简介 竞拍算法(Auction Algorithm)是一种用于解决分配问题的分布式算法,经相关文献查询,最早由麻省理工学院信息和决策系统实验室的Bertsekas(教授?)提出 1,于1988年发表于期刊Annals of Operations Research。 顾名思义,竞拍算法本质就是模拟人类拍.

weixin_47546390的博客 3万+

一起学Netty(六)之 TCP粘包拆包场景

TCP编程底层都有粘包和拆包机制,因为我们在C/S这种传输模型下,以TCP协议传输的时候,在网络中的byte其实就像是河水,TCP就像一个搬运工,将这流水从一端转送到另一端,这时又分两种情况: 1)如果客户端的每次制造的水比较多,也就是我们常说的客户端给的包比较大,TCP这个搬运工就会分多次去搬运。 2)如果客户端每次制造的水比较少的话,TCP可能会等客户端多次生产之后,把所有的水一起

u010154380的专栏 528

摄像头模组CCM简介

摄像头模组,全称Camera Compact Module,简写为CCM。CCM 包含几大件: 镜头(lens)、音圈马达(VCM)、传感器(sensor)、软板(FPC)、图像处理芯片(DSP)。工作原理:物体通过镜头(lens)聚集的光,通过CMOS或CCD集成电路,把光信号转换成电信号,再经过内部图像处理器(ISP)转换成数字图像信号输出到数字信号处理器(DSP)加工处理,转换成标准的GRB、YUV等格式图像信号。摄像头模组主要由以下几个部分组成:镜头(Lens):将光线汇聚到图像传感器上。

m0_73482095的博客 1万+

TCP问题分析

TCP问题分析 网络的五层协议 物理层 数据链路层 网络层,IP协议,ICMP协议(ping) 传输层,传输层有两个协议,面向连接的TCP和无连接的UDP,TCP是点对点的可靠连接,保证数据顺序必达,UDP是无连接的,不保证数据顺序必达,UDP的传输效率要比TCP高,但是可能会丢包,而且一个UDP分段最多只能发送65535个字节,TCP则是数据流的形式进行数据传输的,对于应用层来说,并没

我想要台时光机 3977

第二十七届华东杯数学建模A 题 跳台滑雪问题 完整思路模型及代码

考虑到滑行过程中的空气阻力和摩擦力对速度的影响,我们引入了不同姿态下运动员迎风面积和空气阻力系数的变化,利用牛顿第二定律建立加速度表达式,并通过数值模拟比较常见姿态下的终端速度。针对问题二,我们构建了二维空中飞行动力学模型,结合起跳速度、身体迎角、滑雪板倾角等因素,模拟运动员在空中飞行的轨迹与姿态变化,采用欧拉法对运动过程进行仿真,优化飞行路径和稳定性。仿真结果表明,运动员应在起跳后保持滑雪板与身体成“V”字形打开、重心略后倾的姿态,以增强升力、延长飞行距离并提高姿态得分。,并模拟出跳跃的轨迹和最终得分。

lichensun的博客 3209

(10)Proteus 8.9上的STM32仿真系列(HAL)---freertos

实验设计:通过cubemxide生成freertos工程,然后创建一个 pc13 led灯间隔200ms闪烁的任务。非常遗憾的是proteus8.9仿真失败,实物上没有问题,但还是把流程过一遍。 一、原理图 实物是淘宝上stm32f103 c8t6的核心板,proteus上没有这个芯片,这里图上大概表示一下,用的是核心板自带的pc13引脚的led灯。 二、cubemxide配置 1.RCC如之前工程一样配置 2、配置SYS timebase Source不要使用systick,自己选择一个定时器,我这

qq_35442550的博客 7342

TCP之服务器端接收SYN请求段

这篇笔记要说明的是TCP服务器端在收到客户端第一个SYN请求段时的处理过程,由于SYN请求段的处理过程属于TCP数据接收流程中的一部分,而接收流程非常的复杂,所以下面的代码流程只列出与本主题相关的内容。 1. 接收SYN请求段 TCP层的数据包输入接口为tcp_v4_rcv(),我们从这里看起: int tcp_v4_rcv(struct sk_buff *skb) { struct tcphd...

九天小哥的专栏 3093

SAP SmartForms字体 :从OTF缓存溢出到完美解决

精准诊断:遇到错误,首先区分是图片过大还是字体嵌入导致。删除图片测试是最快的判别方法。字体上传规范始终从SE73入口上传用于表单打印的字体。强烈建议应用SAP Note 1301018,确保上传时勾选“不替换其他字体”,避免系统自动替换引发的意外问题。如需为同一字体族上传粗体、斜体等变体,请在SE73初始屏幕上勾选对应的属性框(如BOLD),然后分别上传对应的字体文件(如上传过程中,系统会提示输入RTF控制序列信息,这是确保打印预览正常显示的关键步骤,必须正确填写。方案选型快速止血:采用方案一(

Pegasus666的博客 294

TCP系列08—连接管理—7、TCP 常见选项(option)

一、TCP选项概述 在前面介绍TCP头的时候,我们说过tcp基本头下面可以带有tcp选项,其中有些选项只能在连接过程中随着SYN包发送,有些可以延后。下表汇总了一些tcp选项其中我标记为红色的部分是常见的TCP选项,我们仅针对这些红色的TCP选项进行介绍(主要是非红色的我也不太了解~~~),另外RFC1323已经被RFC7323取代,这里给出的是TCP选项原始定义的RFC ...

weixin_30881367的博客 1334

OpenClaw Manager Pro - 可视化管理工具

OpenClaw Manager Pro 是一个现代化的 Web 管理界面,用于管理多个 OpenClaw Gateway 实例。🎨美观的界面:深色主题,渐变设计,现代化 UI📡实时监控:WebSocket 实时推送状态和日志🔧完整管理:创建、编辑、删除、启动、停止 Gateway📊多渠道支持:飞书、Telegram、Discord、Slack、微信、钉钉🧩技能管理:可视化配置 61+ 个技能🌍跨平台:支持 macOS、Linux、Windows使用自定义模型系统会自动在中创建},

Maynor的博客 3085

Pydoll浏览器自动化框架教程

让浏览器自动化回归本质。不再纠缠驱动兼容、不再手动处理验证码、不再因为请求和 UI 分裂而头疼。如果你经常做 Web 自动化、爬虫或者测试,不妨试试 Pydoll。也别忘了给项目点个 ⭐,让作者知道有人喜欢这个工具。👉GitHub 地址。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 204

传输层协议TCP—RTTM(11)

1 RTTM(往返时间测量) 往返时间测量确实没有什么好讲述的,除非遇到了超时重传,如图5-123所示: 上图5-123中,对于超时重传场景,如前文所述,实在是没法测量报文 SEG1 的往返时间。针对这种场景,Karn 算法的思路是:既然无法测量 RTT 那就不测量,直接将 RTO(准确地说是 SRTT)进行指数回退。应该说,Karn 算法是一种解决思路,但是有点过于简单粗暴,毕竟对于超时重传场景,不是绝对的无法测量,只是 TCP 机制上有点缺陷致使 A...

weixin_45537413的博客 2866

基于YOLOv26的智能农业无人机巡检系统

本文提出了一种基于YOLOv26的智能农业无人机巡检系统。系统采用模块化设计,包含数据采集、预处理、目标检测、图像拼接、分析决策和部署控制六大核心模块。无人机搭载多光谱相机、RGB相机和热红外相机进行农田数据采集,通过YOLOv26实现作物病虫害、杂草及灌溉设备的检测。系统采用PyTorch、OpenCV等技术栈,支持无人机自动路径规划和多传感器数据采集,并包含数据增强和预处理功能。该系统可有效提升农业巡检效率,为精准农业提供智能化解决方案。

一键难忘的博客 807

TCP(传输控制协议)

详解UDP和TCP一级目录二级目录三级目录 一级目录 二级目录 三级目录

weixin_45599288的博客 6059

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

传输控制协议TCP

TCP(传输控制协议)是一种面向连接、可靠的传输层协议,通过序号、确认、重传等机制保证数据传输的可靠性。它采用三次握手建立连接,通过滑动窗口实现流量控制,并使用慢启动、拥塞避免等算法进行拥塞控制。TCP报文首部包含端口号、序号、确认号、窗口大小等字段,支持全双工通信。连接释放采用四次挥手过程,并设置TIME-WAIT状态确保可靠关闭。

2301_79609207的博客 944

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

4.3 传输控制协议TCP

TCP 协议通过可靠的连接、流量控制、拥塞控制等机制,确保在不可靠的 IP 网络上实现可靠的数据传输。本章详细讲解了 TC P协议的基本概念、主要特点以及套接字和 TCP 连接的管理。

2302_80253507的博客 1689

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

TCP协议-TCP连接管理

一、TCP概述 TCP协议是 TCP/IP 协议族中一个非常重要的协议。它是一种面向连接、提供可靠服务、基于字节流的传输层通信协议。 TCP(Transmission Control Protocol,传输控制协议)。 1.1 TCP协议的特点 (1)TCP 是面向连接的传输层协议。这就是说,通信双方在使用TCP协议进行通信之前,必须先建立TCP连接。在通信结束后,必须释放已经建立的TCP连接。这就好比打电话,通话前要先拨号建立连接,通话结束后要挂机释放连接。 (2)TCP 是点对点(一对一)

yunfan 6945

03 网络编程 TCP传输控制协议

TCP全称 Transmition Control Protocol,即:传输控制协议。是面向连接的协议。通常,TCP 通信还会被冠以 可靠传输协议 的头衔。但请注意,这里的可靠并非指发出去的数据对方一定能收到(这是不可能的),而仅指TCP能使发送方可靠地知道对方是否收到了数据。

qq_59111928的博客 1150

TCP 传输控制协议——详细

TCP 是的运输层协议。每一条 TCP 连接(endpoint),每一条 TCP 连接只能是点对点的(一对一)。

m0_64621282的博客 4983

传输层协议TCP详解(上篇)

传输层协议TCP详解上集,干货满满!!!

小白成长之路 7122

php 通讯协议tcp,TCP(传输控制协议)

# TCP(传输控制协议)> 传输控制协议(Transmission Control Protocol,TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。> 在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。## 一、TCP/IP协议族> TCP/IP(TCP/IP Pro...

weixin_28760959的博客 1411

TCP:传输控制协议

0、TCP传输控制协议; 1、TCP连接的建立与终止; 2、TCP的交互数据流; 3、TCP的成块数据流; 4、TCP超时与重传; 5、TCP坚持定时器; 6、TCP保活定时器; 7、TCP的未来和性能; (零)TCP传输控制协议 1、TCP和UDP都使用相同的网络层IP,TCP却向应用层提供与UDP完全不同的服务。TCP提供一种面向连接、可靠的字节流服务; 2、TCP通过以下方式提供可靠性: 1)应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的数据报长度将保持不变。由TCP

weixin_44895008的博客 240

TCP传输控制协议

服务端每收到一次客户端的请求后都会复位保活计时器(一般2小时),计时器为0时,服务端主动发送探测报文,并每隔75秒发送一次。最初,TCP的接口窗口===接收缓 存区大小。在TCP中,发送方可以通过接收方的反馈,在超时时间到达前,意识到数据包丢失的现象,并进行重传。MSS受到MTU(最大传输单元,指的是数据链路层在封装成帧时所允许携带的最大数据量,在以太网中,MTU数值为1500字节)的限制。当需要进行分片时,发现该报文无法分片,设备会丢弃该报文,并回复ICMP报文(数据不可达),同时携带当前设备MTU。

ZZZKKKRTSAE的博客 992

TCP---传输控制协议

是一种面向连接的可靠传输协议(需要逻辑通道)

m0_74817158的博客 2037
上一篇: Python源码解读之二 万物皆对象(下)
下一篇: Python源码解读之三 对象的创建
jilingwei_123
博客等级 码龄11年 6粉丝 12原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值