1. 概览
1.1 概念
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC793定义。TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
TCP 处于 OSI 模型 的传输层,都属于传输层协议。
1.2 总览
项目 | TCP |
---|---|
面向连接 | 面向连接 |
可靠性 | 可靠传输,使用流量控制和拥塞控制 |
数据传输速度 | 较快 |
首部开销 | 20-60字节 |
传输方式 | 面向字节流 |
交互方式 | 单播(1:1) |
用途 | 网页浏览(HTTP)、电子邮件(SMTP)、文件传输(FTP)和在线视频流 |
优点 | 可靠传输、具有确认应答、超时重传、流量控制、拥塞控制,有序传输,数据的完整性高 |
缺点 | 传输效率较快、资源消耗较多 |
面向连接:相对于另一个传输层协议UDP(User Datagram Protocol, 用户数据报协议而言的。TCP在开始传输数据前要先经历三次握手建立连接,并通过连接一对一发送消息,传输结束后通过四次挥手断开连接。
面向字节流:TCP是面向字节流的传输,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。
1.3 网络层中数据结构
1.4 在GB/T 28181协议中的应用
版本 | TCP应用细化 |
---|---|
GB/T 28181-2011 | 只支持信令交互 |
GB/T 28181-2016 | - 视音频流传输 - 信令交互 |
GB/T 28181-2022 | - 优化的TCP媒体传输机制 - 支持更高效的数据封装与传输 - 强化安全机制和加密传输选项 |
1.5 头部结构解析
- 源端口号(Source Port)(16位->2个字节):发送端口,范围为0-65535。
- 目的端口号(Destination Port)(16位->2个字节):接收端口,范围为0-65535
- 序列号(Sequence Number)(32位->4个字节):说明序列号的范围是[0, 2^32-1],也就是[0, 4294967295]。当序号增加到4294967295后,下一个序号将回到0重新开始。在建立连接时由计算机生成的随机数作为其初始值(ISN,即Initial Sequence Number,初始序列号),通过 SYN 包传给接收端主机,每发送一次数据,就累加一次该“数据字节数”的大小。序列号用来解决网络包乱序问题,实现可靠的数据传输和流量控制。
防止伪造序号,ISN推荐实现方式
-
确认号(Acknowledgment Number)(32位->4个字节):只有在ACK标志位被设置时才有效。它指示期望接收的下一个字节的序列号(所以该字段一般都是上次接收成功的数据字节序号加1),用于确认已经成功接收的数据。在TCP连接建立后,确认号的范围通常是相对于初始序号(ISN)的相对偏移量。如果ISN的初始值为X,那么确认号的范围就是[X+1, X+1+N-1],其中N表示已经成功接收的字节数。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。确认号的范围是[0, 2^32-1],也就是[0, 4294967295]。
-
数据偏移(Data Offset)(4位):指示TCP报文的“数据”起始处距离TCP报文起始处的距离有多远,以4字节为单位计算出的数据段开始地址的偏移值。没有选项时该值为5,即20字节;4位能表示的最大整数是15,也就说明TCP报文里数据开始的位置距离报文起点是60个字节(4*15)。
-
保留(Reserved)(4位):保留供将来使用,目前应设置为零。
-
控制位(Acknowledgment)(8位):
-
URG(Urgent)(1位):指示报文段中包含紧急数据。当URG=1时,表明开启了urgent mode,通知接收方在处理数据时要特别注意紧急数据的处理。URG标志位的设置与紧急指针字段(Urgent Pointer)一起使用。
-
ACK(Acknowledgment)(1位):指示确认号字段有效。仅当ACK=1时确认号字段才有效,当ACK=0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
-
PSH(Push)(1位):指示接收方应立即将数据推送给应用程序,而不是等待缓冲区填满。当两个应用进程进行交互式的通信时,有时一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。这时,发送方TCP把PSH置为1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快地(即“推送”向前)交付接收应用进程。而不用再等到整个缓存都填满了后再向上交付。
-
RST(Reset)(1位):用于复位连接,中断当前的通信。当 RST=1 时,表示 TCP 连接中出现异常(如主机崩溃或其他原因)必须强制断开连接,然后再重新建立连接进行传输。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。
-
SYN(Synchronize)(1位):用于建立连接,发起连接请求。在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
-
FIN(Finish)(1位):用于关闭连接,请求终止连接。当FIN=1时,表示发送方没有数据要传输了,要求释放连接。
-
-
窗口大小(Window Size)(16位->2个字节):指示接收方的接收窗口大小,用于流量控制,最大的窗口大小为2^16-1=65535=64k。这是早期的设计,对于现在的网络应用,可能会不太够,因此可以在选项里加一个窗口扩大选项,来传输更多的数据。窗口指的是发送本报文段一方的接收窗口(而不是自己的发送窗口。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据。
-
校验和(Checksum)(16位->2个字节):奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。
此外,校验和还包括一个概念上位于TCP头部前的96位(12字节)伪头部。这个伪头部包含了源地址、目的地址、协议类型以及TCP的长度信息。这样的设计给予了TCP保护措施,以防段被错误路由。这部分信息虽然存在于Internet协议中,但通过TCP与网络层(IP)接口间的交互(例如调用时的参数或返回结果),在TCP层面上可访问并用于校验和计算过程。
计算方式:把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。把TCP报头中的校验和字段置为0(否则就陷入鸡生蛋还是蛋生鸡的问题),用反码相加法累加所有的16位字(进位也要累加)。最后,对计算结果取反,作为TCP的校验和。
-
紧急指针(Urgent Pointer)(16位->2个字节):只有在URG标志位被设置时才有效。它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,在紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为0时也可以发送紧急数据。
-
选项(Options):可选字段,长度可变,最长可达40个字节。当没有使用选项时,TCP的首部长度是20字节。选项字段用于提供额外的功能和控制,每个选项的开始是 1 字节的kind字段,说明选项的类型。一些常见的选项举例如下:
-
最大报文段长度(Maximum Segment Size, MSS):占用4字节,通常在创建连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
-
窗口扩大因子(Window Scale Factor):占用3字节,取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
-
时间戳选项(TCP Timestamps Option, TSopt):占用10字节,其中最主要的字段是时间戳字段(Timestamp Value field, TSval, 4字节)和时间戳回送回答字段(Timestamp Echo Reply field, TSecr, 4字节)。时间戳选项允许通信的两端在TCP报文段中包含时间戳值,以便进行一些时间相关的操作和计算。
-
安全摘要选项(TCP Authentication Option, TCP Option):用于提供数据完整性和身份验证的功能。该选项用于对TCP报文段进行保护,防止数据篡改和未经授权的访问。
-
-
填充字段(Padding):这个字段中加入额外的零,以保证TCP头是32的整数倍。
1.6 三次握手
TCP传输控制协议是面向连接的、可靠的、基于字节流的传输协议。数据传输是在连接的基础上进行的,三次握手指的是需要发送三次包才能建立连接。
握手信号(SYN)一般是客户端作为主动方的,服务端作为被动方。
1.6.1 三次握手数据包
1.6.2 握手流程
-
首先,客户端和服务端都处于CLOSE状态。先是服务端主动监听某个端口,处于LISTEN状态。
-
客户端主动发起连接SYN(即发送握手信号,要将SYN标志位置为1),发送一个数据包包含序列号和SYN,之后客户端处于SYN-SEND状态。
可见序号被wireShark转义为0,为第一次握手
- 服务端收到发起的连接,返回SYN,并且确认客户端的SYN,之后处于SYN-RCVD状态。
确认号也为1,flag中ack标志位也变为1
- 客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态,因为它一发一收就成功了。
- 服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收了。
握手为什么不是四次:若是服务端发送给客户端的数据包中,SYN和ACK分开发送则需要四次。但是TCP协议格式中的ACK和SYN标志位可以同时为1,互不影响,所以可以同时发送,没有必要进行四次握手。
握手为什么不是两次:假设客户端在发送完SYN后立刻关闭连接,若是两次握手,服务端会认为已经建立与客户端了连接,而客户端并没有建立连接。会出现状态的错误,后续无法进行数据的发送。
CLOSED 初始状态,表示没有任何连接。但是这个状态时虚拟状态,实际并不存在,netstat -ant | grep 端口的时候是找不到。
LISTEN Server端的某个Socket正在监听来自远方的TCP端口的连接请求,随时准备有Client前来连线。在AS启动后,端口处于LISTEN状态。
SYN_SENT (客户端状态,只在客户端出现) 发送连接请求后等待确认信息。当客户端Socket进行Connect连接时,会首先发送SYN包,随即进入SYN_SENT状态,然后等待匹配的连接请求
SYN_RECEIVED (服务端状态,只在服务端出现) 收到一个连接请求后回送确认信息和对等的连接请求,然后等待确认信息。通常是建立TCP连接的三次握手过程中的一个中间状态,表示Server端的Socket接收到来自Client的SYN包,并作出回应。
ESTABLISHED 表示连接已经建立,可以进行数据传输。在Client端出现表示Client把自己的ACK(第3次握手)发出去了,Client已经就绪;在Server端出现表示Server已经收到Client的ACK(第3次握手)了,Server已经就绪。只有在Client和Server同时为ESTABLISHED时,即同时就绪时才可以进行数据传输。
1.7 四次挥手
- 主动方打算关闭连接,此时会发送一个TCP首部FIN标志位被置为1的报文,也即报文,之后主动方进入FIN_WAIT_1状态。
- 被动方收到该报文后,就向主动方发送ACK应答报文,接着被动方进入CLOSED_WAIT状态。
-
主动方收到被动方的ACK应答报文后,之后进入FIN_WAIT_2状态。
-
等待被动方处理完数据后,也向主动方发送FIN报文,之后被动方进入LAST_ACK状态。
- 主动方收到被动方的FIN的报文后,回一个ACK应答报文,之后进入TIME_WAIT状态。
-
被动方收到了ACK应答报文后,就进入了CLOSED状态,至此被动方已经完成了连接的关闭。
-
主动方在经过2MSL一段时间后,自动进入CLOSED状态,至此主动方也完成连接的关闭。
为什么是四次挥手
因为在主动方发送FIN时,可能数据包里还带有有效数据没有处理完,为了确保所有数据都能到达,并且被动方能够处理完所有数据,需要处理一段时间。所以被动方的FIN断开连接请求不能和ACK数据报一起发送,要等待一段时间以后单独发送一个断开连接的数据报。因此需要四次挥手。
MSL是TCP协议规定的报文在网络中的最大生存时间,通常设置为2分钟,因此2MSL大约是4分钟。在这段时间过后,连接正式关闭,相关资源得以回收。
FIN_WAIT_1(仅出现在主动方) 主动关闭连接的一方向对方发送FIN包(表示己方不再有数据需要发送)后,则进入FIN_WAIT_1状态,等待对方返回ACK包。在该状态下,主动方此后还能读取数据,但不能发送数据。在正常情况下,无论对方处于何种状态,都应该马上返回ACK包,所以FIN_WAIT_1状态一般很难见到。
FIN_WAIT_2(仅出现在主动方) 主动关闭连接的一方收到对方返回的ACK包后,等待对方发送FIN包。处于FIN_WAIT_1状态下的Socket收到了对方返回的ACK包后,便进入FIN_WAIT_2状态。由于FIN_WAIT_2状态下的Socket需要等待对方发送的FIN包,所有常常可以看到。若在FIN_WAIT_1状态下收到对方发送的同时带有FIN和ACK的包时,则直接进入TIME_WAIT状态,无须经过FIN_WAIT_2状态。
TIME_WAIT(仅出现在主动方) 主动关闭连接的一方收到对方发送的FIN包后返回ACK包(表示对方也不再有数据需要发送,此后不能再读取或发送数据),然后等待足够长的时间(2MSL)以确保对方接收到ACK包(考虑到丢失ACK包的可能和迷路重复数据包的影响),最后回到CLOSED状态,释放网络资源。操作系统通常会将2MSL设为4分钟,最低不少于30秒,因而TIME_WAIT状态一般维持在30秒至4分钟
CLOSE_WAIT(仅出现在被动方) 示被动关闭连接的一方在等待关闭连接。当收到对方发送的FIN包后(表示对方不再有数据需要发送),相应的返回ACK包,然后进入CLOSE_WAIT状态。在该状态下,若己方还有数据未发送,则可以继续向对方进行发送,但不能再读取数据,直到数据发送完毕。
LAST_ACK(仅出现在被动方) 被动关闭连接的一方在CLOSE_WAIT状态下完成数据的发送后便可向对方发送FIN包(表示己方不再有数据需要发送),然后等待对方返回ACK包。收到ACK包后便回到CLOSED状态,释放网络资源。
CLOSING 比较罕见的例外状态。正常情况下,发送FIN包后应该先收到(或同时收到)对方的ACK包,再收到对方的FIN包,而CLOSING状态表示发送FIN包后并没有收到对方的ACK包,却已收到了对方的FIN包。有两种情况可能导致这种状态:其一,如果双方几乎在同时关闭连接,那么就可能出现双方同时发送FIN包的情况;其二,如果ACK包丢失而对方的FIN包很快发出,也会出现FIN先于ACK到达。
2. 工作原理
2.1 数据包发送与接收流程
2.1.1 数据包发送流程
- 数据分段:根据MTU(最大传输单元)和当前窗口大小,发送方可能需要将数据拆分成多个数据段。每个数据段包含TCP头部和数据负载,每个数据段都有序列号,表示它在数据流中的位置。
- 添加TCP头部:每个数据段前加上TCP头部,包含源端口、目的端口、序列号、确认号、数据偏移(头部长度)、保留位、标志位(如SYN、ACK、FIN、RST等)、窗口大小、校验和、紧急指针等字段。
- 封装成IP数据包:TCP数据段被封装进IP数据包中,再由网络层处理,加上源IP地址、目的IP地址等信息。
- 物理层传输:最终,数据包通过物理网络发送出去。
2.1.2 数据包接收流程
- 接收数据包:网络层接收到IP数据包后,剥离IP头部,将TCP数据段传递给传输层。
- 校验与排序:
- 校验和:TCP头部包含校验和,接收方计算并验证数据段的完整性。
- 排序:根据数据段的序列号对它们进行排序,确保数据的正确顺序。
- 确认与流量控制:
- 发送ACK:接收方发送ACK,确认已接收到的数据段。ACK中的确认号是对收到序列号的下一个期待数据段的确认。
- 窗口通告:接收方通过TCP头部中的窗口大小字段,告知发送方其缓冲区的可用空间大小,实现流量控制。
- 数据传递给应用层:排序和校验后的数据段被重新组合成原始数据,然后传递给应用层。
2.1.3 抓包示例
2.2 重传机制
但是在复杂的网络环境下,并不一定能顺利的进行数据传输,例如数据包丢失,针对这种问题,TCP 使用了重传机制来解决。
常见的重传方式有以下几种:
- 超时重传(Timeout Retransmission) : 当发送方发出一个数据段后,会启动一个计时器。如果在这个计时器超时前没有收到对应的确认应答(ACK),则认为数据段可能丢失,进而重新发送该数据段。超时时间通常是基于过去的往返时延(Round-Trip Time, RTT)动态调整的。
- 快速重传(Fast Retransmit) : 如果接收方收到一个失序的数据段,它会发送一个重复的ACK,即再次确认最后一个按序接收的数据段的ACK。如果发送方收到连续的三个或更多的重复ACK,无需等待超时,将立即重传疑似丢失的数据段,这是快速重传机制。
- 选择性确认(Selective Acknowledgment, SACK) : SACK允许接收方通知发送方哪些数据段已经收到,哪些段缺失。这样,发送方可以只重传那些真正丢失的数据段,而不是整个数据流,提高了效率。
- 重复SACK(Duplicate SACK, DSACK) : 重复SACK进一步细化了快速重传机制,通过更加详细的反馈告知发送方数据包的确切丢包情况,帮助发送方更好地判断是否需要进行快速重传以及哪些数据需要重传。
2.2.1 超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传。
TCP 会在以下两种情况发生超时重传:
- 数据包丢失
- 确认应答丢失
RTT(Round Trip Time):往返时延,也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各自独立的 RTT。
RTO(Retransmission Time Out):重传超时,也就是前面说的超时时间。
当 RTO < RTT 时, 将会触发大量的重传, 当 RTO > RTT 时候, 如果频繁出现丢包, 重传不及时, 又会造成网络的反应慢, 最好的结果是 RTO 略大于 RTT。
2.2.2 快速重传
快速重传机制RFC5681基于接收端的反馈信息来引发重传,而非重传计时器超时。
刚刚提到过,基于计时器的重传往往要等待很长时间,而快速重传使用了很巧妙的方法来解决这个问题:服务器如果收到乱序的包,也给客户端回复 ACK,只不过是重复的 ACK。举个例子来说,收到乱序的包 6,7,8,9 时,服务器全都发 ACK = 5。这样,客户端就知道 5 发生了空缺。一般来说,如果客户端连续三次收到重复的 ACK,就会重传对应包,而不需要等到计时器超时。
但快速重传仍然没有解决第二个问题:到底该重传多少个包?
2.2.3 选择性确认-SACK
还有一种实现重传机制的方式叫:SACK
( Selective Acknowledgment 选择性确认)。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有[发送1]这段数据丢失,则重发时,就只选择了这个TCP段进行重复。
2.2.4 重复SACK-DSACK
2.4 滑动窗口
滑动窗口机制允许发送方和接收方之间实现流量控制和可靠性传输。发送方可以持续发送数据而不需要等待每个数据段的确认,从而提高传输效率。接收方可以根据自身的处理能力和缓冲区空间调整窗口大小,从而控制发送方窗口大小。
用接收窗口大小控制发送窗口大小,再由发送窗口大小控制发送速率。
2.4.1 基本概念
在TCP中,发送方维护一个发送窗口,接收方则会维护一个接收窗口,它们是一个连续的字节序列,表示发送方可以发送的数据范围大小。窗口由两个参数定义:窗口的起始字节和窗口的大小。
发送方将数据分成多个数据段,按顺序发送到接收方。每个数据段都包含一个序列号,标识数据在发送方发送窗口中的位置。接收方使用ack确认应答报文来通知发送方已成功接收数据 ,随后发送方通过ack报文窗口滑动。ack字段值表示接收方期望接收的下一个字节的序列号。
发送窗口:发送方维护的一个窗口,表示当前可以发送而不必等待确认的数据段序列号范围。发送窗口的大小由接收方通告的窗口大小(即接收窗口)和拥塞窗口共同决定,取两者的最小值。
接收窗口:接收方告知发送方其当前可用于接收数据的缓存大小。当接收方处理(或丢弃)数据后,会通过ACK报文更新窗口大小。
窗口滑动:当发送方收到接收方的ACK,确认了之前发送的某个数据段,发送窗口就会向前滑动,释放出空间用于发送新的数据。同时,接收方的接收窗口也会根据数据处理情况向前滑动。
2.4.2 工作原理
- 初始化:连接建立时,双方都会通告一个初始的窗口大小。
- 数据发送:发送方在发送数据时,会跟踪已经发送但尚未确认的数据,以及下一个待发送的数据段的序列号。
- 接收确认:接收方接收到数据后,会发送ACK确认报文,其中包含期望接收的下一个数据段的序列号(即当前已接收到的最大序列号加一)。
- 窗口调整:
- 流量控制:如果接收方的缓存接近满载,会减小通告窗口大小,限制发送方的发送速率。
- 拥塞控制:当网络出现拥塞迹象时,发送方会减小拥塞窗口大小,减少数据的发送,直到网络状况改善。
- 滑动与重传:如果发送的数据在一定时间内没有收到ACK,发送方可能会执行重传机制。同时,发送窗口可能因为重传而暂时“倒退”,但随后会随着新确认的到来再次向前滑动。
2.4.3 示例图
Sent and Acknowledged:这些数据表示已经发送成功并已经被确认的数据,比如图中的前31个bytes,这些数据其实的位置是在窗口之外了,因为窗口内顺序最低的被确认之后,要移除窗口,实际上是窗口进行合拢,同时打开接收新的带发送的数据
Send But Not Yet Acknowledged:这部分数据称为发送但没有被确认,数据被发送出去,没有收到接收端的ACK,认为并没有完成发送,这个属于窗口内的数据。
Not Sent,Recipient Ready to Receive:这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,其实这个窗口是完全有接收方告知的,接收方告知还是能够接受这些包,所以发送方需要尽快的发送这些包
Not Sent,Recipient Not Ready to Receive: 这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了发送端所接收的范围
Left edge和Right edge:分别表示滑动窗口的左边界和右边界。
Usable Window:表示窗口的缓冲区。
Send Window :发送窗口, 这部分值是有接收方在三次握手的时候进行设置的,同时在接收过程中也不断的通告可以发送的窗口大小,来进行适应。
Window Already Sent: 已经发送的数据,但是并没有收到 ACK。
- 假设[32-45] 数据,是上层Application发送给TCP的,TCP将其分成四个Segment来发往internet
- seg1 32-34 seg3 35-36 seg3 37-41 seg4 42-45 这四个片段,依次发送出去,此时假设接收端之接收到了seg1 seg2 seg4
- 此时接收端的行为是回复一个ACK包说明已经接收到了32-36的数据,并将seg4进行缓存(保证顺序,产生一个保存seg3 的hole)
- 发送端收到ACK之后,就会将32-36的数据包从发送并没有确认切到发送已经确认,提出窗口,这个时候窗口向右移动
- 假设接收端通告的Window Size仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴
- 对于丢失的seg3,如果超过一定时间,TCP就会重新传送(重传机制),重传成功会seg3 seg4一块被确认,不成功,seg4也将被丢弃 就是不断重复着上述的过程,随着窗口不断滑动,将真个数据流发送到接收端,实际上接收端的Window Size通告也是会变化的,接收端根据这个值来确定何时及发送多少数据,从对数据流进行流控。
2.5 流量控制
发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
接下来根据流量图来说明过程:
- 客户端是接收方,服务端是发送方
- 接收窗口和发送窗口相同,都为
200
- 客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。
- 服务端收到请求报文后,发送确认报文和 80 字节的数据,于是可用窗口
Usable
减少为 120 字节,同时SND.NXT
指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。 - 客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节,
RCV.NXT
也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。 - 服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法再继续发送数据。
- 客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节,
RCV.NXT
也就指向 441,接着发送确认报文给服务端。 - 服务端收到对 80 字节数据的确认报文后,
SND.UNA
指针往右偏移后指向 321,于是可用窗口Usable
增大到 80。 - 服务端收到对 120 字节数据的确认报文后,
SND.UNA
指针往右偏移后指向 441,于是可用窗口Usable
增大到 200。 - 服务端可以继续发送了,于是发送了 160 字节的数据后,
SND.NXT
指向 601,于是可用窗口Usable
减少到 40。 - 客户端收到 160 字节后,接收窗口往右移动了 160 字节,
RCV.NXT
也就是指向了 601,接着发送确认报文给服务端。 - 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是
SND.UNA
指针偏移了 160 后指向 601,可用窗口Usable
也就增大至了 200。
2.6 拥塞控制
为什么要有拥塞控制呀,不是有流量控制了吗?
前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….
所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。
于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。
什么是拥塞窗口?和发送窗口有什么关系呢?
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd
和接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
那么怎么知道当前网络是否出现了拥塞呢?
其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。
拥塞控制有哪些控制算法?
2.6.1. 慢启动(Slow Start)
- 原理:开始时,发送方的拥塞窗口(cwnd)设得很小,然后每接收到一个确认(ACK),cwnd大小翻倍,直到达到慢启动阈值(ssthresh)或检测到拥塞。
- 目的:逐步试探网络的承载能力,避免一开始就发送大量数据导致网络拥塞。
2.6.2. 拥塞避免(Congestion Avoidance)
- 原理:当拥塞窗口达到慢启动阈值后,拥塞避免算法开始生效,此时每收到一个ACK,cwnd不再是加倍增长,而是线性增加,比如每经过一个往返时间(RTT)增加1个MSS(最大段大小)。
- 目的:在已知网络可承受范围内,逐步增加数据发送量,避免网络过载。
2.6.3. 快重传(Fast Retransmit)
- 原理:如果发送方连续收到三个或更多的重复ACK(表明某个数据段丢失),则立即重传丢失的数据段,无需等待重传计时器超时。
- 目的:快速响应数据丢失,减少因等待重传超时引起的延迟。
2.6.4. 快恢复(Fast Recovery)
- 原理:在执行快重传后,拥塞窗口不回退到初始值,而是设置一个新的阈值(通常是当前cwnd的一半),并进入拥塞避免阶段,以较快速度恢复到新的阈值。
- 目的:在检测到少量数据包丢失后迅速恢复,同时避免网络过度反应。
2.7 系统中使用TCP协议的常用的程序使用特定端口号
- HTTP (Hypertext Transfer Protocol) - 用于网页浏览,端口号 80。随着HTTPS加密协议的普及,HTTP逐渐被HTTPS(端口443)替代。
- HTTPS (HTTP Secure) - 加密的HTTP协议,端口号 443。
- SSH (Secure Shell) - 安全远程登录和文件传输协议,端口号 22。
- SMTP (Simple Mail Transfer Protocol) - 邮件传输协议,端口号 25。对于加密邮件传输,通常使用 587 或 465(SSL/TLS)。
- POP3 (Post Office Protocol version 3) - 邮件接收协议之一,端口号 110,加密版本通常使用 995。
- IMAP (Internet Message Access Protocol) - 另一邮件接收协议,用于邮件管理,端口号 143,加密版本使用 993。
- FTP (File Transfer Protocol) - 文件传输协议,控制连接端口号 21,数据连接通常使用动态分配的端口。
- DNS (Domain Name System) - 域名解析服务,尽管主要使用UDP,但也监听TCP端口 53。
- MySQL - 数据库服务,常用端口号 3306。
- HTTP Proxy - 代理服务器,常用端口号 8080 或 8888。
- Telnet - 远程终端访问协议,端口号 23。
- SNMP (Simple Network Management Protocol) - 网络管理协议,通常使用 161(UDP)和 162(Trap)。
3. TCP生产问题总结
3.1 粘包问题
-
问题
- 连续发送数据包时粘连:发送方连续发送多个数据包,由于TCP为了提高传输效率,可能会将这些数据包合并成一个较大的数据包发送给接收方,导致接收方难以区分原始的独立数据包。
- 数据包拆分:同样,TCP在传输过程中也可能将一个较大的数据包拆分成多个小的数据片段进行发送,接收方需要重新组装这些片段。
-
解决
- 定长消息:发送固定长度的消息体,接收方按照这个长度来读取数据,这样就可以明确区分每个消息。但这种方法灵活性较差,不适合消息体长度变化的场景。
- 消息边界标识:在消息末尾添加特殊字符或序列作为消息结束的标记,接收方通过查找这些边界标识来分割消息。例如,使用特定的分隔符(如'\n')或固定长度的结束标记。
- 长度字段:在每个消息前面添加长度字段,指定该消息的总长度,接收方首先读取长度字段,然后根据该长度读取完整的消息内容。这种方式比较灵活,适用于消息长度可变的情况。
- 协议设计:设计更为复杂的协议格式,包括消息头和消息体,消息头中可以包含消息类型、消息长度等信息,接收方先解析消息头,再根据消息头中的信息正确解析消息体。
- 使用更高层次的协议:如HTTP、WebSocket等应用层协议,它们内部已经处理了消息的分界问题,开发者无需直接处理TCP的粘包问题。
3.2 0窗口
在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
窗口关闭潜在的危险
接收方向发送方通告窗口大小时,是通过 ACK
报文来通告的。
那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
TCP 是如何解决窗口关闭时,潜在的死锁现象呢?
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
- 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
- 如果接收窗口不是 0,那么死锁的局面就可以被打破了。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST
报文来中断连接。
3.3 死锁状态
3.3 发送优化
3.4 糊涂窗口综合症
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
要知道,我们的 TCP + IP
头有 40
个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。
就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。
现举个糊涂窗口综合症的栗子,考虑以下场景:
接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:
- 接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;
- 在下一个发送方的 TCP 段到达之前,应用程序还从缓冲区中读取了 40 个额外的字节;
每个过程的窗口大小的变化,在图中都描述的很清楚了,可以发现窗口不断减少了,并且发送的数据都是比较小的了。
所以,糊涂窗口综合症的现象是可以发生在发送方和接收方:
- 接收方可以通告一个小的窗口
- 而发送方可以发送小数据
于是,要解决糊涂窗口综合症,就解决上面两个问题就可以了
- 让接收方不通告小窗口给发送方
- 让发送方避免发送小数据
3.5 连接耗尽与TIME_WAIT状态
- 问题:短时间内大量短连接的创建和关闭会导致端口资源耗尽,尤其是服务器端存在大量处于TIME_WAIT状态的连接。
- 解决:调整
net.ipv4.tcp_tw_reuse
和net.ipv4.tcp_tw_recycle
参数,启用TIME_WAIT socket重用和快速回收机制,或者使用更长连接的策略减少连接建立和关闭的频率。
4. 安全性
TCP的安全性主要体现在以下几个方面:
-
校验和(Checksum) :TCP头包含一个校验和字段,用于检测数据在传输过程中是否发生错误。这个校验和覆盖了TCP头部和数据负载,能够检测出大多数的数据损坏情况,但无法防范恶意篡改,因为校验和容易被重新计算。
-
序列号和确认应答(Sequence and Acknowledgment Numbers) :TCP通过序列号确保数据包按照正确的顺序被接收,并通过确认应答机制确保数据的成功传输。这有助于防止数据包的重复和乱序,但同样不防篡改。
-
超时重传和快速重传机制:这些机制有助于在网络不稳定或数据包丢失时恢复数据传输,保持连接的稳定性和可靠性,但它们不增强安全性。
-
流量控制和拥塞控制:通过滑动窗口、慢启动等机制,TCP可以控制数据的发送速率,避免拥塞和数据丢失,间接提升通信的健壮性,但这仍然是在数据传输层面而非安全层面。
-
安全增强措施:
- IPSec:可以在网络层为TCP数据提供加密和验证,保护数据在传输过程中的机密性和完整性。
- SSL/TLS:在应用层使用HTTPS(HTTP over SSL/TLS)或其他加密协议,对TCP传输的数据进行加密,确保数据的私密性和完整性,同时提供服务器身份验证。
- TCP Wrapper:作为一种主机级别的访问控制工具,可以限制哪些主机可以访问特定的TCP服务,增强系统安全性。
- 防火墙和入侵检测系统:可以用来过滤恶意的TCP连接请求,保护网络和系统免受攻击。
5. TCP调优策略
TCP调优策略旨在优化网络性能,减少延迟,提高吞吐量,并确保数据传输的可靠性。
5.1. TCP窗口大小调整
- 原理:TCP窗口大小决定了发送方在接收到确认之前可以发送多少数据。通过调整
net.core.rmem_max
(接收窗口)和net.ipv4.tcp_wmem
(发送窗口)等内核参数,可以增加单次传输的数据量,从而提高吞吐量。 - 实践:使用
sysctl
命令动态调整,或修改/etc/sysctl.conf
永久设置。例如,增加发送窗口大小:
#bash
sysctl -w net.ipv4.tcp_wmem="4096 87380 16777216"
5.2. 拥塞控制算法选择
- 原理:不同的拥塞控制算法适应不同的网络环境。Linux提供了多种算法,如CUBIC(默认)、BBR、NewReno等。
- 实践:使用
sysctl
调整net.ipv4.tcp_congestion_control
参数,选择合适的算法。例如,切换到BBR算法:
echo bbr > /sys/module/tcp_bbr/parameters/tcp_congestion_control
5.3. TCP延迟确认
- 原理:延迟确认策略允许接收方在一定时间内积累多个ACK,减少网络中的ACK流量,但可能增加数据传输的延迟。
- 实践:调整
net.ipv4.tcp_delack_max
参数来控制延迟时间,或完全禁用延迟确认。
5.4. MTU(最大传输单元)优化
- 原理:通过设置适当的MTU大小,可以减少IP分片,提高传输效率。PMTUD自动发现功能可以帮助找到最优MTU。
- 实践:根据网络设备和路径配置,手动设置或启用PMTUD,调整
ip link set dev eth0 mtu 1500
(以太网卡默认值)。
5.5. SACK(选择性确认)和DSACK(重复SACK)
- 原理:SACK允许接收方通知发送方哪些数据已收到,哪些部分丢失,使得重传更加精确。DSACK帮助检测重复数据。
- 实践:确保系统支持并启用这些功能,一般情况下现代Linux系统默认开启。
5.6. 快速打开(TCP Fast Open)
- 原理:允许在三次握手的同时发送数据,减少首次数据传输的延迟。
- 实践:通过设置
net.ipv4.tcp_fastopen
参数启用,并配置客户端和服务端。
5.7. TCP参数硬调与软调
- 原理:硬调是指即时生效但重启后恢复默认的设置,软调则是持久化修改。
- 实践:使用
sysctl
命令进行临时调整,修改/etc/sysctl.conf
实现持久化。
5.8. TCP KeepAlive设置
- 原理:定期发送探测包检测连接是否存活,避免无效连接占用资源。
- 实践:调整
net.ipv4.tcp_keepalive_time
(开始探测的时间间隔)、tcp_keepalive_intvl
(探测间隔)和tcp_keepalive_probes
(探测次数)。
5.9. 优化网络堆栈
- 原理:减少CPU上下文切换和中断处理时间,提高网络数据处理效率。
- 实践:调整网络队列长度、使用RSS(Receive Side Scaling)分散处理、开启XPS(eXpress Data Path)等。
5.10. 监控与测试
- 原理:通过工具如
netstat
、ss
、tcpdump
、iperf
等监控网络状态和性能,评估调优效果。 - 实践:定期检查网络状态,进行压力测试,根据测试结果微调参数。