2022-12-25 TCP/IP 协议栈_6


老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>



前言

TCP协议是TCP/IP协议栈另一最重要的协议, 属于真正要操作的部分, 很多重要socket选项都和它相关, 如果理解不到位, 可能影响到编程的效率.

TCP有四个问题需要注意, 首先是头部信息, 它出现在每个TCP报文中, 指定通信的源端端口号, 目的端端口号, 管理TCP连接, 控制双向数据流.

TCP状态转移过程, TCP连接的每端都是一个状态机, 从建立连接到断开, 两端的状态机将经历不同的状态变迁. 对于调试网络应用程序, 对TCP状态转移的理解是必须的.

TCP数据流, 我们可以通过分析TCP数据流了解应用层协议和通信双方交换应用数据.我们将讨论两种类型的TCP数据流, 交互数据流和成块数据流,简单了解紧急数据.

TCP数据流的控制, 内核对TCP数据流的控制保障传输及网络通信质量. 讨论包括超时重传及拥塞控制.


一、TCP服务的特点

TCP协议相对于UDP协议, 其特点是面向连接, 字节流以及可靠传输.

面向连接意味着使用TCP协议通信的双方要先连接, 后进行数据读写.

双方都要分配内核资源管理连接的状态和连接数据传输. TCP连接是全双工的,双方数据通过一个连接进行.而完成数据交换, 双方必须断开连接以释放系统资源.

由于TCP属于一对一连接, 所以基于广播和多播的应用不适合.而无连接的UDP协议则非常合适.

当发送端连续多次写操作, TCP模块先将数据放入缓冲区, 而真正发送数据时, 数据可能被封装成一个或多个TCP报文段发出. 因此, TCP模块发送除的TCP报文段的个数和应用程序执行的写操作次数没有固定的数量关系.

而接收端接收报文后,TCP模块将它们携带的应用程序数据按照报文段序号依次放入TCP接收缓冲区中,并通知应用程序读取数据.而读取也可以是多次的, 这取决于读取缓冲区的大小.因此,应用程序执行的读操作次数与接收的报文段个数也没有固定的数量关系.

综上,发送的写操作次数和接收的读操作次数没有数量关系,这就是字节流的概念.应用程序对数据的发送和接收是没有边界的限制的.

UDP则不是这种机制, 发送端每进行一次写操作, UDP就会封装发送, 接收端如不即时收取进行读操作, 就会丢包. 并且, 如果没有足够的缓冲区进行读取, 则UDP也会截断.

TCP传输是可靠的, 发送的报文必须得到接收方的应答才算成功, 同时TCP采取超时重传机制, 如果在某个时间内无应答, 则进行重传. 最后TCP还要对无序的IP报文中的TCP报文进行整理, 再交付应用层.

UDP协议则提供不可靠服务, 需要上层协议处理数据确认和超时重传.

二、TCP文件头

TCP头部信息在每个TCP报文中, 指定通信源端口和目的端口, 管理连接, 保证实现信息的可靠.

我们仍然是打开Windows终端, WSL终端, wireshark捕获WSL网络, 开启WSL的telnet服务.

通过Windows端的telnet客户端连接 WSL服务, 进行捕获:

0000   06 0e 00 17 84 4f 77 82 00 00 00 00 80 02 fa f0   .....Ow.........
0010   7e e0 00 00 02 04 05 b4 01 03 03 08 01 01 04 02   ~...............
16位源端口号16位目的端口号32位序号32位确认号4位头部长度3位保留, Nonce, CWR, ECN-EchoURG, ACK, PSH
RST, SYN, FIN
16位窗口大小16位校验和16位紧急指针选项,最多40字节
06 0e00 1784 4f 77 8200 00 00 001000000000000010fa f07e e000 0002 04 05 b4 01 03 03 08 01 01 04 02
1550, 客户端位临时端口号23, 服务端使用知名服务端口号2219800450, A至B第一个报文为随机值, 后续报文为此值加数据第一字节在字节流的偏移0, 用作对另一方发送来的TCP报文的响应, 其值是收到的TCP报文的序号之加1, 此报文为第一次发送所以为08, 标识该TCP头右多少个4字节, TCP头最多60字节3位0, NS: 拥塞控制随机和, CWR(Congestion Window Reduced) - 拥塞窗口减少, ECN-Echo显式拥塞通知URG紧急指针, ACK确认号有效, PSH提示应用程序立即从缓冲读取数据, RST重新建立连接, SYN请求建立连接, FIN通知对方本端要关闭连接64240, 接收通告窗口, 告诉对方本端TCP接收缓冲区还可容纳字节数由发送端填充, 接收端执行CRC算法检验TCP报文在传输过程是否损坏,校验包括头部和数据一个正偏移量, 它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号在下方详细解释

选项:

1字节kind1字节lengthn字节info说明
kind=0选项表结束选项
kind=1空操作选项(NOP)
kind=2length=4最大segment长度(2字节)最大报文段长度选项, 一般 MSS = MTU - 40 字节
kind=3length=3移位数(1字节)窗口扩大因子选项.TCP连接初始化时, 通信双方使用该选项协商接收通告窗口的扩大因子. 假设TCP头部中的接收通告窗口大小是N, 窗口扩大因子(位移数)是M, 则实际接收通告窗口的大小是 N * 2^M. 和MSS选项一样, 窗口扩大因子选项只能出现在同步报文段中, 否则将被忽略.
kind=4length=2选择性确认选项.TCP通信时, 如果某个TCP报文段丢失, 则TCP模块会重传最后被确认的TCP报文段后续所有报文段, 产生了重复发送, 降低了性能. SACK可改善此种情况, 使得TCP模块只重新发送丢失的TCP报文段, 不用发送所有未被确认的TCP报文段.
kind=5length=N*8+2第一块左边沿, 第一块右边沿…第N块左边沿, 第N块右边沿是SACK实际工作的选项. 该选项参数告诉发送方本端已经收到并缓存的不连续的数据块, 从而让发送端可以据此检查并重发丢失的数据块. 每个块边沿参数包含一个4字节的序号. 其中块左边沿表示不连续块的第一个数据的序号, 而块有边沿则表示不连续块的最后一个数据的序号的下一个序号. 这样一对参数之间的数据是没有收到的. 因为一个块信息占用8字节, 所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)
kind=8length=10时间戳值(4字节), 时间戳回显应答(4字节)时间戳选项. 该选项提供了较为准确的计算通信双方之间的回路时间的方法, 为TCP流量控制提供重要信息

以下是wireshark抓包自动解析的数据:

Transmission Control Protocol, Src Port: 1550, Dst Port: 23, Seq: 0, Len: 0
    Source Port: 1550
    Destination Port: 23
    [Stream index: 0]
    [Conversation completeness: Incomplete, DATA (15)]
    [TCP Segment Len: 0]
    Sequence Number: 0    (relative sequence number)
    Sequence Number (raw): 2219800450
    [Next Sequence Number: 1    (relative sequence number)]
    Acknowledgment Number: 0
    Acknowledgment number (raw): 0
    1000 .... = Header Length: 32 bytes (8)
    Flags: 0x002 (SYN)
        000. .... .... = Reserved: Not set
        ...0 .... .... = Nonce: Not set
        .... 0... .... = Congestion Window Reduced (CWR): Not set
        .... .0.. .... = ECN-Echo: Not set
        .... ..0. .... = Urgent: Not set
        .... ...0 .... = Acknowledgment: Not set
        .... .... 0... = Push: Not set
        .... .... .0.. = Reset: Not set
        .... .... ..1. = Syn: Set
        .... .... ...0 = Fin: Not set
        [TCP Flags: ··········S·]
    Window: 64240
    [Calculated window size: 64240]
    Checksum: 0x7ee0 [unverified]
    [Checksum Status: Unverified]
    Urgent Pointer: 0
    Options: (12 bytes), Maximum segment size, No-Operation (NOP), Window scale, No-Operation (NOP), No-Operation (NOP), SACK permitted
        TCP Option - Maximum segment size: 1460 bytes
            Kind: Maximum Segment Size (2)
            Length: 4
            MSS Value: 1460
        TCP Option - No-Operation (NOP)
            Kind: No-Operation (1)
        TCP Option - Window scale: 8 (multiply by 256)
            Kind: Window Scale (3)
            Length: 3
            Shift count: 8
            [Multiplier: 256]
        TCP Option - No-Operation (NOP)
            Kind: No-Operation (1)
        TCP Option - No-Operation (NOP)
            Kind: No-Operation (1)
        TCP Option - SACK permitted
            Kind: SACK Permitted (4)
            Length: 2
    [Timestamps]
        [Time since first frame in this TCP stream: 0.000000000 seconds]
        [Time since previous frame in this TCP stream: 0.000000000 seconds]

三、TCP连接的建立和关闭

1. 用wireshark观察TCP连接的建立和关闭

Windows终端通过telnet登录WSL, 抓取报文:

登录后的状态:

Linux DESKTOP-0T82VB6 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Dec 30 09:44:46 CST 2022 from DESKTOP-0T82VB6.mshome.net on pts/1
lhb@DESKTOP-0T82VB6:~$

退出后状态:

欢迎使用 Microsoft Telnet Client

Escape 字符为 'CTRL+]'


Microsoft Telnet> quit

E:\clangC++>

建立连接过程:

No.timesourcedestinationprotocollengthinfo
10.000000172.20.16.1172.20.26.235TCP6632064 → 23 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
20.000209172.20.26.235172.20.16.1TCP6623 → 32064 [SYN, ACK] Seq=0 Ack=1 Win=64240 Len=0 MSS=1460 SACK_PERM=1 WS=128
30.000277172.20.16.1172.20.26.235TCP5432064 → 23 [ACK] Seq=1 Ack=1 Win=2102272 Len=0

第一个TCP报文段包含一个SYN标识, 所以是一个同步报文段, 即 Windows客户端发送连接请求给WSL服务端. 起始 seq 为 一个随机数, 相对偏移为0.

第二个报文是WSL端回复Windows端, 同意建立连接, 确认的相对seq为0, ack 为 第一个同步报文段序号相对值加一, 也就是 1. 序号值是用来标识TCP数据流中的每一个字节的, 同步报文段较特殊, 即使它并没有携带任何应用程序数据, 也要占用一个序号值.

第三个TCP报文段是Windows端对WSL服务端回复的确认, 至此, TCP连接建立. 这个步骤称之为三次握手.

从第 1 个TCP报文段开始, wireshark 捕获的简述的序号值都是相对初始序号值的偏移, 如果要绝对值, 可以看详细信息.

断开连接过程:

No.timesourcedestinationprotocollengthinfo
7817.317913172.20.16.1172.20.26.235TCP544372 → 23 [FIN, ACK] Seq=74 Ack=711 Win=2101504 Len=0
7917.318394172.20.26.235172.20.16.1TCP5423 → 4372 [FIN, ACK] Seq=711 Ack=75 Win=64256 Len=0
8017.318439172.20.16.1172.20.26.235TCP544372 → 23 [ACK] Seq=75 Ack=712 Win=2101504 Len=0

一般来讲, 断开过程服务端在收到客户端的断开请求 [FIN, ACK] 的报文后, 会先返回一个应答报文[ACK], 然后再返回确认断开报文[FIN,ACK], 我用 telnet 测试, 如果是正常途径退出, 是没有第一个应答报文的, 也就是四次挥手缺了一次, 但我如果是强制退出, exit logout, 则会有这个应答报文, 也就是完整的四次挥手.

No.timesourcedestinationprotocollengthinfo
295642.750725172.20.26.235172.20.16.1TELNET62Telnet Data: logout\r\n,
TCP 23 → 5570 [FIN, PSH, ACK] Seq=609 Ack=76 Win=64256 Len=8
296642.750784172.20.16.1172.20.26.235TCP545570 → 23 [ACK] Seq=76 Ack=618 Win=2101760 Len=0
297642.751285172.20.16.1172.20.26.235TCP545570 → 23 [FIN, ACK] Seq=76 Ack=618 Win=2101760 Len=0
298642.751421172.20.26.235172.20.16.1TCP5423 → 5570 [ACK] Seq=618 Ack=77 Win=64256 Len=0

关闭报文的时候, 首先客户端要发送一个含有[FIN]标识的报文, 即Windows客户端要求关闭. 结束报文和同步报文一样,也要占用一个序号值. WSL服务端会先发送一个报文来确认此结束报文段, 然后发送带有[FIN]标识的结束报文段. Windows客户端发送最后一个报文予以确认. 当然如上一个三报文表格所示, 服务端在接收到结束请求报文后, 不是必须发送确认报文的, 可以只发送最终的含[FIN]报文段的回复报文, 从而省略确认报文, 当然, 省略机制和TCP延迟确认特性有关.

在连接的关闭过程, 由哪方先发送结束报文段, 则哪方就是主动关闭, 对方是被动关闭.

一般来说, TCP连接时由客户端发起, 通过三次握手建立连接. TCP的关闭过程相对复杂, 可能是客户端主动关闭, 可能是服务端主动关闭.同时关闭也有可能, 只是比较少见.

2. 半关闭状态

TCP连接是全双工的, 它允许两个方向的数据传输被独立关闭.

比如, 客户端可发送结束报文给服务端, 告诉它本端已经完成了数的发送, 但允许继续接收来自对方的数据, 直至对方发送结束报文段. 这种状态被称为半关闭状态.

socket 网络编程接口通过shutdown函数提供了对半关闭的支持, 但使用半关闭的应用程序很少见.

3. 连接超时

正常的快速连接我们已经讨论过, 我们接下来讨论超时情况.

当客户端访问一个远方服务器, 可能由于网络繁忙, 服务器对客户端发送的同步报文段没有应答, 此时客户端则需要进行重连, 此过程可能发生多次, 如果重连仍然无效, 则通知应用程序连接超时.

使用wireshark进行超时抓包:

首先通过iptables命令过滤数据包, 丢弃所有接收到的连接请求, 客户端就无法得到任何确认报文段

在WSL服务端过滤tcp连接请求报文段:

~$ sudo iptables -I INPUT -p tcp --syn -i eth0 -j DROP

开启Windows终端, 通过telnet 连接WSL服务端:

telnet 172.20.29.139
正在连接172.20.29.139...无法打开到主机的连接。 在端口 23: 连接失败

抓包数据:

1	0.000000	172.20.16.1	172.20.29.139	TCP	66	1349923 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
2	1.005851	172.20.16.1	172.20.29.139	TCP	66	[TCP Retransmission] [TCP Port numbers reused] 1349923 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
3	3.009278	172.20.16.1	172.20.29.139	TCP	66	[TCP Retransmission] [TCP Port numbers reused] 1349923 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
6	7.020917	172.20.16.1	172.20.29.139	TCP	66	[TCP Retransmission] [TCP Port numbers reused] 1349923 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
11	15.030855	172.20.16.1	172.20.29.139	TCP	66	[TCP Retransmission] [TCP Port numbers reused] 1349923 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1

五次连接请求, 发送时间间隔分别为1, 2, 4, 8秒,共四次超时重连操作, 每次重连时间翻倍, 四次失败后, TCP模块放弃连接, 并通知应用程序.

四、TCP状态转移

TCP连接是有状态的, 我们看看从TCP连接到关闭的整个过程中通信两端的状态变化.

TCP从连接到关闭的状态转移图
服务器通过listen调用进入LISTEN状态, 被动等待客户端连接, 即被动打开.

服务器一旦监听到连接请求(同步报文), 就将连接放入内核等待队列, 并向客户端发送SYN确认报文段, 此时该连接处于SYN_RCVD状态.

当服务器成功接收到客户端发送的确认报文段, 则该连接转移到ESTABLISHED状态, 即双方能够进行双向数据传输的状态.

当客户端主动关闭连接(通过close或shutdown)向服务器发送结束报文段, 服务器通过确认报文段使得连接进入CLOSE_WAIT状态. 此状态的含义是等待服务器应用程序关闭连接. 通常, 服务器监测到客户端关闭连接后, 也会立即给客户端发送一个结束报文段来关闭连接. 使得连接转移到LAST_ACK状态, 等待客户端对结束报文端的最后一次确认. 一旦确认完成, 连接彻底关闭.

接下来我们关注客户端的状态转移.

客户端通过connect调用主动与服务器建立连接, 首先给服务器发送一个同步报文段,使得连接转移到SYN_SENT状态. connect系统调用可能因连个原因失败: 如果connect连接的目标断开不存在或该端口仍被处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect调用失败.如果目标端口存在, 但connect在超时时间内未收到服务器确认报文段,则connect调用失败.

connect调用失败将使得连接立即返回到初始的CLOSED状态.如果客户端成功收到服务器同步报文段确认,connect调用成功返回, 连接转移至ESTABLISHED状态.

当客户端执行主动关闭时,它向服务器发送一个结束报文段,同时进入 FIN_WAIT_1 状态.若客户端手段服务器专门用于确认目的的确认报文段,则将状态转移至 FIN_WAIT_2 状态 ( 也可能省略此状态 ), 此时服务器处于CLOSE_WAIT状态, 这一对状态可能发生版关闭的状态. 此时如果服务器也关闭连接发送结束报文段, 则客户端确认并进入TIME_WAIT状态.

处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段才能转移至TIME_WAIT状态, 否则它将一直停留此状态, 如果不是为了在版关闭状态下继续接收数据, 连接长时间停留在FIN_WAIT_2状态毫无益处. 连接停留在FIN_WAIT_2状态的情况可能发生在客户端执行版关闭后, 未等服务器关闭连接就强行退出了.此时客户端连接由内核接管, 可称孤儿连接. Linux系统定义了两个内核变量以指定内核能接管的孤儿连接数即孤儿连接在内核中生存的时间.

TIME_WAIT状态: 客户端连接在收到服务器的结束报文段后, 并没有直接接入CLOSED状态, 而是转移到TIME_WAIT状态. 在这个状态, 客户端连接要等待一段长为2MSL(Maximum Segment Life, 报文段最大生存时间, 一般为2min)才完全关闭. TIME_WAIT 状态存在的原因有两点: 1. 可靠的终止TCP连接, 2. 保证让迟来的TCP报文段有足够的时间被识别并丢弃.

一个TCP连接处于TIME_WAIT状态时,我们无法立即使用该连接占用着的端口建立新连接,否则新连接(连接化身)可能即受到属于原来的连接,导致错误.TCP报文最大生存时间是MSL,两个MSL时间的TIME_WAIT状态能确保网络上的两个传输磅巷上尚未被接收到的,迟到的TCP报文段都已经消失. 因此一个新连接可以在2MSL时间后安全建立.

有时候, 我们希望避免TIME_WAIT状态, 在客户端一般不存在这个问题, 因为客户端端口是随机的, 不会发生立即连接等待, 但服务端不一样, 当服务端主动关闭连接后异常终止, 则因为它使用同一个知名服务端口号, TIME_WAIT状态将导致它不能立即重启. 我们可通过socket选项 SO_REUSEADDR强制进程立即使用处于TIME_WAIT状态的连接占用的端口.

五、TCP复位报文段

在某种情况下, TCP连接的一端会向另一端发送RST报文段, 即复位报文段, 以通知对方关闭连接或重新建立连接.

我们通过wireshark抓一段不存在端口的报文: Windows端通过telnet 连接 WSL, 但是要连接不存在端口:

E:\clangC++>telnet 172.20.19.152 54324
正在连接172.20.19.152...无法打开到主机的连接。 在端口 54324: 连接失败

报文内容:

19	310.655826	172.20.16.1	172.20.19.152	TCP	66	333854324 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
20	310.655997	172.20.19.152	172.20.16.1	TCP	54	543243338 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0
21	311.161045	172.20.16.1	172.20.19.152	TCP	66	[TCP Retransmission] [TCP Port numbers reused] 333854324 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1

报文内容是Windows端发起同步报文, 请求连接, 服务端回复RST, 通知对方关闭连接或重新连接, Windows端重新发起连接请求.

还有一种情况, 上一部分讲到的, 服务端处于TIME_WAIT状态, 也会发送RST报文段.

异常终止连接, TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段,一旦发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃.应用程序可以通过socket选项SO_LINGER发送复位报文段,异常终止连接.

处理半打开连接:当服务器或客户端关闭或异常终止连接,而对方没有接收到结束报文段,还维持着原来的连接,而断开的一方已经没有该连接的任何信息了.我们称这种状态为半打开状态,处于这种状态的连接成为半打开连接.如果客户端或服务器往处于半打开状态的连接写入数据,则对方回应一个复位报文段.

六、TCP交互数据流

接下来我们讨论通过TCP连接交换的应用程序数据. TCP报文段按照所携带数据长度可分为交互数据和成块数据.

交互数据仅包含很少的字节, 使用交互数据的应用程序或协议对实时性要求较高, 如telnet, ssh等.

成块数据的长度则通常为TCP报文段允许的最大数据长度. 使用成块数据的应用程序或协议对传输小路要求高, 比如ftp.

我们还是通过Windows端telnet连接到WSL, 并通过wireshark抓取报文, 执行ls命令.

No.TimeSourceDestinationProtocolLengthInfo
10.000000172.20.16.1172.20.19.124TELNET55Telnet Data …
Data: l
20.000699172.20.19.124172.20.16.1TELNET55Telnet Data …
Data: l
30.048425172.20.16.1172.20.19.124TCP5416841 → 23 [ACK] Seq=2 Ack=2 Win=8209 Len=0
40.175580172.20.16.1172.20.19.124TELNET55Telnet Data …
Data: s
50.176405172.20.19.124172.20.16.1TELNET55Telnet Data …
Data: s
60.227291172.20.16.1172.20.19.124TCP5416841 → 23 [ACK] Seq=3 Ack=3 Win=8209 Len=0
70.694461172.20.16.1172.20.19.124TELNET56Telnet Data …
Data: \r\n
80.695117172.20.19.124172.20.16.1TELNET66Telnet Data …
Data: \r\n
Data: \033[?2004l\r
90.741957172.20.16.1172.20.19.124TCP5416841 → 23 [ACK] Seq=5 Ack=15 Win=8209 Len=0
100.742210172.20.19.124172.20.16.1TELNET158Telnet Data …
Data: \033[0m\033[01;34mCpp\033[0m \033[01;34mgo\033[0m \033[01;31mxmake-v2.5.9.amd64.deb\033[0m\r\n
Data: \033[?2004hlhb@DESKTOP-0T82VB6:~$
110.789586172.20.16.1172.20.19.124TCP5416841 → 23 [ACK] Seq=5 Ack=119 Win=8209 Len=0

报文段1由客户端发给服务端, 带1字节字母 “l”, 报文2是服务端对报文1的确认, 同时回显 “l”, 报文3是对报文2的确认, 没有内容.

第4至6报文是传递 “s”,

报文7传递回车换行, 报文8回显回车换行, 以及WSL端分隔符, 像是乱码. 报文9是对8的确认没有内容.

报文10是 ls 命令的结果, 有分隔符, 像是乱码, 但客户端解析后就是 ls 命令的返回结果. 最后是完整命令提示, 报文11是确认, 没有内容.

上述过程, 客户端的确认 3,6,9,11都不携带任何应用程序数据, 而服务器发送的确认报文段, 2,5,8,10都包含它需要发送的应用程序数据. 服务器的这种处理方式称为延迟确认, 在一段时间延迟后查看本端是否有数据需发送, 如果有, 则和确认一同发出. 因此, 它发送确认报文段的时候总是有数据一起发送.

延迟确认可减少发送TCP报文段的数量, 而由于用户的输入速度明显鳗鱼客户端的处理速度, 所以客户端的确认报文段总是不携带任何应用程序数据. 在TCP连接的建立和断开过程, 也可能发生延迟确认(4次挥手少一次).

在广域网, 交互数据流可能有很大延迟, 且携带交互数据的微笑TCP报文段数量一般很多, 容易引起拥塞的发生, 需要通过一些算法缓解.

Nagle算法, 可以让TCP连接的通信双发在任意时刻都最多只能发送一个未被确认的TCP报文段, 在该TCP报文段确认到达之前不能发送其它TCP报文段. 同时, 发送方在等待确认的同时手机本端需要发送的微量数据, 并在确认到来时以一个TCP报文段将它们发出.这样可以减少网络上的微小报文段数量. 该算法的另一个优点在于其自适应性, 确认到达越快, 数据发送得越快.

七、TCP成块数据流

我们可以通过FTP协议传输大文件, 观察TCP成块数据流.

首先在WSL端安装vsftpd服务器程序, 并启动:

~$ sudo apt-get install vsftpd

~$ sudo service vsftpd start

在Windows端连接, 通过wireshark进行捕获

E:\clangC++>ftp 172.20.20.66
连接到 172.20.20.66。
220 (vsFTPd 3.0.3)   
200 Always in UTF8 mode.
用户(172.20.20.66:(none)): lhb
331 Please specify the password.
密码:
230 Login successful.
ftp>

在此需要设置Windows防火墙, 让ftp可以接收信息, 否则所有ftp的信息传不到Windows端.

在Windows桌面点击: 开始:

Windows管理工具:

高级安全 Windows defender 防火墙:

入站规则:

文件传送程序(TCP协议的) 允许连接: 应用, 启用规则.

不放心的可以在作用域加上Windows端和WSL端的内部模拟 IP 地址. 这样其它IP地址的ftp都传不进来.

ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
Cpp
go
xmake-v2.5.9.amd64.deb
226 Directory send OK.
ftp: 收到 36 字节,用时 0.0036.00千字节/秒。
ftp> get xmake-v2.5.9.amd64.deb
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for xmake-v2.5.9.amd64.deb (1300830 bytes).
226 Transfer complete.
ftp: 收到 1300830 字节,用时 0.001300830.00千字节/秒。
ftp>

我们通过 ls 命令查看服务器端的文件,

No.TimeSourceDestinationProtocolLengthInfo
93.907218172.20.16.1172.20.20.66FTP77Request: PORT 172,20,16,1,36,3
103.907700172.20.20.66172.20.16.1FTP105Response: 200 PORT command successful. Consider using PASV.
113.914053172.20.16.1172.20.20.66FTP60Request: NLST
123.915065172.20.20.66172.20.16.1TCP7420 → 9219 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=2346769492 TSecr=0 WS=128
133.915227172.20.16.1172.20.20.66TCP669219 → 20 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=256 SACK_PERM=1
143.915451172.20.20.66172.20.16.1TCP5420 → 9219 [ACK] Seq=1 Ack=1 Win=64256 Len=0
153.915521172.20.20.66172.20.16.1FTP93Response: 150 Here comes the directory listing.
163.915576172.20.20.66172.20.16.1FTP-DATA87FTP Data: 33 bytes (PORT) (NLST)
173.915576172.20.20.66172.20.16.1TCP5420 → 9219 [FIN, ACK] Seq=34 Ack=1 Win=64256 Len=0
183.915597172.20.16.1172.20.20.66TCP549219 → 20 [ACK] Seq=1 Ack=35 Win=2102272 Len=0
193.915776172.20.20.66172.20.16.1FTP78Response: 226 Directory send OK.
203.915799172.20.16.1172.20.20.66TCP549213 → 21 [ACK] Seq=30 Ack=115 Win=7975 Len=0
213.922129172.20.16.1172.20.20.66TCP549219 → 20 [FIN, ACK] Seq=1 Ack=35 Win=2102272 Len=0
223.922335172.20.20.66172.20.16.1TCP5420 → 9219 [ACK] Seq=35 Ack=2 Win=64256 Len=0

FTP传输的方法和telnet不同, 客户端发出命令, 服务端接收, 确认并返回信息, 客户端确认并返回信息, 服务端会另开端口请求连接, 一旦客户端确认, 就连续发送TCP报文, 将FTP信息及FTP-DATA数据连续发给客户端, 并在最后发送FIN报文, 请求断开新开的端口. 客户端则收到多次TCP报文后返回一个确认. 服务端返回FTP信息, 客户端确认, 客户端确认断开新端口, 服务端确认, 至此, 一个完整的FTP传输完成.

通过 get 文件名, 获取文件到Windows端本地磁盘.

No.TimeSourceDestinationProtocolLengthInfo
10.000000172.20.16.1172.20.20.66FTP79Request: PORT 172,20,16,1,37,141
20.000518172.20.20.66172.20.16.1FTP105Response: 200 PORT command successful. Consider using PASV.
30.007678172.20.16.1172.20.20.66FTP83Request: RETR xmake-v2.5.9.amd64.deb
40.008440172.20.20.66172.20.16.1TCP7420 → 9613 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=2348484925 TSecr=0 WS=128
50.008599172.20.16.1172.20.20.66TCP669613 → 20 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=256 SACK_PERM=1
60.008794172.20.20.66172.20.16.1TCP5420 → 9613 [ACK] Seq=1 Ack=1 Win=64256 Len=0
70.008873172.20.20.66172.20.16.1FTP139Response: 150 Opening BINARY mode data connection for xmake-v2.5.9.amd64.deb (1300830 bytes).
80.008931172.20.20.66172.20.16.1FTP-DATA7354FTP Data: 7300 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
90.008931172.20.20.66172.20.16.1FTP-DATA7354FTP Data: 7300 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
100.008959172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=14601 Win=2102272 Len=0
110.009062172.20.20.66172.20.16.1FTP-DATA14654FTP Data: 14600 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
120.009089172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=29201 Win=2102272 Len=0
130.009145172.20.20.66172.20.16.1FTP-DATA14654FTP Data: 14600 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
140.009175172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=43801 Win=2102272 Len=0
150.009227172.20.20.66172.20.16.1FTP-DATA2974FTP Data: 2920 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
160.009241172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=46721 Win=2102272 Len=0
170.009294172.20.20.66172.20.16.1FTP-DATA21954FTP Data: 21900 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
180.009294172.20.20.66172.20.16.1FTP-DATA4434FTP Data: 4380 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
190.009351172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=73001 Win=2097920 Len=0
200.009398172.20.20.66172.20.16.1FTP-DATA5894FTP Data: 5840 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
210.009415172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=78841 Win=2092032 Len=0
220.009505172.20.20.66172.20.16.1FTP-DATA23414FTP Data: 23360 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
230.009505172.20.20.66172.20.16.1FTP-DATA38014FTP Data: 37960 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
240.009505172.20.20.66172.20.16.1FTP-DATA32174FTP Data: 32120 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
250.009623172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=172281 Win=1998592 Len=0
260.009733172.20.20.66172.20.16.1FTP-DATA29254FTP Data: 29200 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
270.009781172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=201481 Win=1969408 Len=0
280.009914172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
290.009914172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
300.009914172.20.20.66172.20.16.1FTP-DATA35094FTP Data: 35040 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
310.009914172.20.20.66172.20.16.1FTP-DATA26334FTP Data: 26280 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
320.010165172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=385441 Win=1785344 Len=0
330.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
340.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
350.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
360.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
370.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
380.010489172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
390.010489172.20.20.66172.20.16.1FTP-DATA32174FTP Data: 32120 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
400.011171172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=785481 Win=1385472 Len=0
410.011495172.20.20.66172.20.16.1FTP-DATA29254FTP Data: 29200 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
420.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
430.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
440.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
450.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
460.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
470.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
480.011495172.20.20.66172.20.16.1FTP-DATA61374FTP Data: 61320 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
490.011495172.20.20.66172.20.16.1FTP-DATA56964FTP Data: 56910 bytes (PORT) (RETR xmake-v2.5.9.amd64.deb)
500.012352172.20.16.1172.20.20.66TCP549613 → 20 [ACK] Seq=1 Ack=1300832 Win=870144 Len=0
510.012615172.20.20.66172.20.16.1FTP78Response: 226 Transfer complete.
520.012654172.20.16.1172.20.20.66TCP549612 → 21 [ACK] Seq=55 Ack=161 Win=7929 Len=0
530.019331172.20.16.1172.20.20.66TCP54[TCP Window Update] 9613 → 20 [ACK] Seq=1 Ack=1300832 Win=2102272 Len=0
540.021847172.20.16.1172.20.20.66TCP549613 → 20 [FIN, ACK] Seq=1 Ack=1300832 Win=2102272 Len=0
550.022115172.20.20.66172.20.16.1TCP5420 → 9613 [ACK] Seq=1300832 Ack=2 Win=64256 Len=0

当接收一个真正意义上的大文件, 服务端会连续发送多个 TCP 报文, 客户端会在接收多个报文段后回复一个确认报文.

服务端得到确认报文后, 知道客户端还有多少缓存, 开始阶段, Windows 的缓存是 2102272, 服务端根据缓存大小连续发送总和不大于缓存的数据. 直至最后传输完毕, Windows 端的缓存为870144, 也就是大部分数据在缓存中等待ftp写入硬盘.

如果看每个FTP Data的TCP报文, 都会有PSH标志, 通知客户端尽快读取数据, 但好像作用不大.

八、带外数据

有些传输层协议有带外数据概念, 可以迅速通告对方本端发生的重要事件. 实际应用中, 带外数据使用较少见, 已知的有telnet, ftp等远程非活跃程序.

UDP没有实现带外数据传输. TCP也没有真正的带外数据, 但通过紧急指针标志和紧急指针字段, 给应用程序提供了一种紧急方式. 后文将TCP紧急数据称为带外数据( 在 <Tcp\Ip详解>中, 作者认为这种叫法是错误的).

TCP发送带外数据时, 会将头部设置URG标志, 并将紧急指针设置为指向最后一个带外数据的下一字节(<Tcp\Ip详解>中, 作者认为这是错误的, 应指向外带数据的最后一个字节, 可见TCP协议对于紧急数据的实现是相当混乱), 发送端发送的多字节带外数据只有最后一个字节被当作带外数据, 其它被当成普通数据.

TCP接收带外数据的过程, 接收端只有接收到URG标志时才检查紧急指针, 然后确定带外数据位置, 并将其读入一个只有一字节大小的带外缓存中. 如果上层应用没有即时将带外数据从带外缓存中读出, 后续带外数据将覆盖它.

上面是TCP带外数据的默认接收方式, 如果我们给TCP连接设置了SO_OOBINLINE选项, 则带外数据江河普通数据一样被TCP模块防止TCP接收缓存中. 紧急指针可以用来指出带外数据的位置, socket编程接口也提供了系统调用来识别带外数据.

内核如何通知应用程序带外数据的到来, 以及应用程序如何发送和接收带外数据, 会在后续讨论.

九、TCP超时重传

我们在上面讨论了正常网络TCP数据报文的运转, 下面讨论网络异常情况, 超时或丢包, 数据传输的控制.

TCP报文在发出时, 会启动重传定时器, 根据超时重传策略进行超时重传.

我们还是通过telnet进行模拟, Windows登录 WSL的telnet服务, 输入 ls 命令, 在回车之前, 通过 iptables 命令截断WSL端的返回确认, 我们看看通过wireshark能捕获到什么

Windows 端连接后输入 ls命令:

~$ ls

WSL端通过iptables命令丢弃返回确认报文, 这样Windows端没有收到确认和回复, 会重新发送确认.
iptables的命令请查看这里

lhb@DESKTOP-0T82VB6:~$ sudo iptables -I OUTPUT -p tcp --tcp-flags ACK, ACK -o eth0 -j DROP

在持续一段时间后, 删除 iptables 的防火墙规则, 恢复服务端的回复:

lhb@DESKTOP-0T82VB6:~$ sudo iptables -F

wireshark抓到的报文:

No.TimeSourceDestinationProtocolLengthInfo
10.000000192.168.112.1192.168.114.98TELNET55Telnet Data …
20.000490192.168.114.98192.168.112.1TCP5423 → 21930 [ACK] Seq=1 Ack=2 Win=502 Len=0
30.000781192.168.114.98192.168.112.1TELNET55Telnet Data …
40.048507192.168.112.1192.168.114.98TCP5421930 → 23 [ACK] Seq=2 Ack=2 Win=8207 Len=0
50.567568192.168.112.1192.168.114.98TELNET55Telnet Data …
60.568102192.168.114.98192.168.112.1TELNET55Telnet Data …
70.616195192.168.112.1192.168.114.98TCP5421930 → 23 [ACK] Seq=3 Ack=3 Win=8207 Len=0
1016.280843192.168.112.1192.168.114.98TELNET56Telnet Data …
1116.591829192.168.112.1192.168.114.98TCP56[TCP Retransmission] 21930 → 23 [PSH, ACK] Seq=3 Ack=3 Win=8207 Len=2
1217.204163192.168.112.1192.168.114.98TCP56[TCP Retransmission] 21930 → 23 [PSH, ACK] Seq=3 Ack=3 Win=8207 Len=2
1318.415225192.168.112.1192.168.114.98TCP56[TCP Retransmission] 21930 → 23 [PSH, ACK] Seq=3 Ack=3 Win=8207 Len=2
1420.821188192.168.112.1192.168.114.98TCP56[TCP Retransmission] 21930 → 23 [PSH, ACK] Seq=3 Ack=3 Win=8207 Len=2
1725.631793192.168.112.1192.168.114.98TCP56[TCP Retransmission] 21930 → 23 [PSH, ACK] Seq=3 Ack=3 Win=8207 Len=2
1827.427263192.168.114.98192.168.112.1TELNET170Telnet Data …
1927.478552192.168.112.1192.168.114.98TCP5421930 → 23 [ACK] Seq=5 Ack=119 Win=8206 Len=0

通过观察报文 10 – 19 可以看到Windows端的超时重传策略, 每隔 0.25s, 0.5s, 1s, 2s, 4s, 发送重传确认, 直至收到确认. 如果更长时间则会断开.

十、拥塞控制

拥塞控制是提高网络利用效率, 降低丢包率, 并保证网络资源对每条数据流的公平性. 拥塞控制的标准文档是RFC 5681, 介绍了拥塞控制的四个部分, 慢启动, 拥塞避免, 快速重传, 快速恢复.

Linux有多种实现,如cubic算法, 它们或部分或全部的实现了上面的四部分.

可以用如下命令查看具体算法:

lhb@DESKTOP-0T82VB6:~$ cat /proc/sys/net/ipv4/tcp_congestion_control
cubic

拥塞控制的最终受控变量是发送端向网路一次连续写入 (收到其中第一个数据的确认之前) 的数据量, 我们称为SWND(Send Window, 发送窗口 ). 不过, 发送端最终以TCP报文段来发送数据, 所以SWND限定了发送端能连续发送的TCP报文段数量.这些TCP报文段的最大长度(仅指数据部分)称为SMSS(Sender Maximum Segment Size)其值一般等于MSS.

接收方可通过接收通告窗口RWND来控制发送端的SWND, 发送端也引入了一个拥塞窗口CWND的状态变量, 实际的SWND值是RWND和CWND中的较小者.

慢启动和拥塞避免:

TCP建立连接后, CWND被设置为初始值 IW (Initial Window)大小为 2–4 SMSS.此时发送端最多能发送 IW 字节数据, 之后发送端每收到接收端一个确认, 其CWND就按照 CWND+=min(N, SMSS) 增加, 其中N式此次确认中包含的之前未被确认的字节数. 这就是慢启动, TCP刚开始发送数据时不知道网络的实际情况, 需要用一种试探的方式平滑地增加CWND的大小.

但CWND的膨胀很快, 不控制会导致网络拥塞. 因此TCP拥塞控制中定义了另一个重要的状态变量: 慢启动门限(slow start threshold size, ssthresh). 当CWND的大小超过该值, TCP拥塞控制进入拥塞避免阶段.

拥塞避免算法使得CWND按照现行方式增加, RFC 5681中提到两种实现: 1, 每个RTT时间内按照 CWND+=min(N, SMSS) 增加, 不论该RTT时间内发送端收到多少确认. 2, 没收到一个对新数据的确认报文段, 就按照CWND+=SMSS*SMSS/CWND进行更新.

以上时发送端未监测到拥塞时所采取的避免拥塞方法, 当拥塞确实发生时, 会有其它拥塞控制行为.

发送端判断拥塞发生的依据是: 1. 传输超时, 或TCP重传定时器溢出. 2. 接收到重复的确认报文段.

队医第一种情况, 仍然使用慢启动和拥塞避免. 对于第二种情况, 则使用快速重传和快速恢复, 注意, 第二种情况如果发生在重传定时器溢出之后, 则也被拥塞控制当成第一种情况对待.

如果发送端监测到拥塞发生时由于传输超时, 及上述第一种情况, 那么它将执行重传并做如下调整: ssthresh = max (FlightSize/2, 2*SMSS), CWMD <= SMSS. 其中FlightSize是已经发送但未收到确认的字节数.如此调整后, CWMD将小于SMSS, 那么也必然小于新的慢启动门限值, 孤儿拥塞控制再次进入慢启动阶段.

快速重传和快速恢复:

很多情况, 发送端都可能收到重复确认报文段, 拥塞控制算法需判断收到重复确认报文段时, 网络是否真的发生了拥塞. 具体算法, 发送端如果连续收到3个重复的确认报文段就是拥塞发生了, 然后它启用快速重传和快速恢复算法来处理拥塞, 过程如下:

1, 当收到3个重复确认报文段是, 按照ssthresh = max (FlightSize/2, 2*SMSS) 计算ssthresh, 然后立即重传丢失的报文段, 并按照 CWND = ssthresh + 3 * SMSS.

2, 每次收到1个重复的确认是, 设置CWND = CWND + SMSS. 此时发送端可以发送新的TCP报文段( 如果新的CWND允许的话 ).

3, 当收到新数据的确认时, 设置CWND = ssthresh ( ssthresh是新的慢启动门限值, 由第一步计算得到 ).

快速重传和杜埃苏恢复完成之后, 拥塞控制将恢复到拥塞避免阶段.


总结

终于写的差不多了, TCP部分比以前讨论的协议都复杂的多, 它的特点是面向连接, 字节流和可靠传输.

我们介绍了TCP的头部信息, 其指定通讯两端的端口号, 管理连接, 控制数据流. 还介绍了TCP状态的转移过程, 从TCP的连接到断开, 两端主机会经历不同的状态变迁. TCP由两种数据流, 交互数据流, 比较短小, 快速, 成块数据流, 比较大, 效率高. 此外还有对数据流的控制, 包括超时传送和拥塞控制.

总之, 内容很多, 需要好好消化, 理解之后, 会对后期的socket编程有所帮助.


老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值