近来因为面试需要,决定重新学习一下TCP协议,这是自己在学习过程中的一些知识总结,希望给用到的同学提供帮助,如若存在疏漏和错误之处,还请大家在文末留言纠正。
1.报文格式
下图给出了TCP数据报的格式:
端口号:用于标识主机上的具体应用。0~65535(0保留),源端口指本机应用标识,目的端口指对方应用标识。
序列号:当前报文段第一个数据字节编号。
确认号:期望收到对方下一个数据序号。序号之前的数据已经接收并释放缓存。
头部长度:TCP数据报首都长度,以4字节为单位。与可选项有关,如果可选项为空,那么首部长度为5。因为头部长度为4bit,所以最大为60字节。
紧急指针:URG置1时,表示为紧急数据报,报文中包含了紧急数据,紧急数据始终存放在报文段数据的开始地方,紧急指针定义了紧急数据在数据区的结束处。
URG:紧急指针
ACK:确认标识,确认报文中置1。
PSH:推送数据,尽快将这个报文交给应用层。
RST:复位连接,对方收到后关闭当前连接。
SYN:同步标识,同步报文中置1。TCP通过三次握手协议建立连接时需要指定此标识。
FIN:终止连接标识,终止连接报文中置1。TCP在关闭连接时需要指定此标识。
窗口大小:以字节为单位,告知对方自己当前可用户接收报文的缓冲区大小,发送端收到报文后查看窗口大小,决定自己下步的数据发送,最大值65535字节。
校验值:校验和包括TCP伪首部、TCP首部、TCP数据区三部分。
可选项:①最大报文段长度(MSS),每个连接通常在通信的第一个报文段(包含SYN标识的连接握手报文)中指明这个选项,用于告诉对方自己所能够接受的最大报文段。MSS值在0~65535之间,默认为536。②窗口扩大因子,可以让通信的双方声明更大的窗口,首部中的窗口字段用于通告本端接收窗口大小,其长度为16bit,最大值为65535字节,但是在一些高速的场合下,这个窗口值有些小,需要声明更大的窗口值,因此就需要在可选项中通过指定窗口扩大因子来向对方通告自己更大的窗口(接收缓存能力),此时通告窗口大小值通过下面方式计算,其中A为窗口扩大因子的值:
通告窗口大小 = 首部中窗口大小 × 2^A
报文中的选项信息总长度必须是4字节的整数倍,如果不是,需要在选项信息后面补0,直至长度满足要求。
2.连接的建立和关闭
为确保数据的可靠传输,TCP协议规定在数据的传输之前要建立连接,同样数据通信完毕后可关闭连接。其实,就是进行数据通信的双方在内部实时维护着一个连接状态,通信完毕后关闭这个任务的维护,并释放相应的资源。
TCP连接的建立采用三次握手的方式,也就是主动方先发起一个建立连接的请求报文(报文中SYN标识置1),被动方接收到之后针对这个请求报文返回一个应答报文(报文中SYN标识置1,ACK标识置1,表示是针对同步请求的应答报文),主动方接收到对方的应答报文后再返回一个应答报文(报文中ACK标识置1,为普通的应答报文)。下图中展示了TCP建立连接和数据传输的整个过程,需要注意的是通信过程中的序列号和确认号。
TCP连接的关闭需要进行四次通信,也被称为“四次挥手”。因为TCP是一种全双工协议,通信的双方都可以随时发送数据给对方,每个方向上的数据传输都是独立的,所以在关闭连接时也是独立的。具体的策略就是: 任何一方在数据发送完毕想要关闭连接时,主动的发送终止报文(FIN位置1),对方收到后返回应答报文(ACK置1),此时只是单向的关闭数据连接,也被叫做“半连接”(但不是真正关闭,还需要等待对方的终止报文请求)。因此,另一方也需要发送终止报文(FIN位置1),并在收到对方的应答报文(ACK置1)之后,整个TCP连接才算完整关闭。具体的同通信过程如下图所示:
3.连接状态维护
TCP作为一种可靠的面向连接的传输协议,核心在于对于整个通信链路的状态维护。TCP定义了11中状态,其状态转移图如下所示:
-
CLOSED
:初始状态,没有连接。 -
LISTEN
:监听状态,一般为服务端,等待客户端发起建立连接请求。 -
SYN_RCVD
:已收到客户端的连接请求,等待ACK报文。 -
SYN_SEND
:已发送连接请求,等待对方SYN+ACK同步报文的确认报文。 -
ESTABLISHED
:连接已经建立,此时双方可以进行数据传输,因为是全双工模式,所以任何一方都可以随时发送数据,彼此是独立的。 -
FIN_WAIT_1
:已发送关闭连接请求,即FIN报文,等待对方的确认报文,或者关闭连接请求报文。 -
FIN_WAIT_2
:已收到对方的确认报文。 -
CLOSING
:已发送关闭连接请求,并收到对方关闭连接请求报文,等待对方的确认报文。 -
CLOSE_WAIT
:收到对端的关闭连接请求,等待应用程序关闭连接,之后发送本端的关闭连接请求。 -
LAST_ACK
:已完成对端的关闭连接请求,并发送了本端的关闭连接请求,等待对端的确认报文。 -
TIME_WAIT
:关闭成功,等待网络中可能出现的剩余数据,等待时间为2MSL(MSL,报文的最大生存时间,大多数实现中取30s,有的取60s)。
上述的状态转移图中,描述了TCP连接在处于不同状态时,因为条件的变动而向其他状态迁移,对于迁移条件的研究应该是重点。如下:
首先,必须要有一方先监听某个端口,等待对方发起建立连接的请求,而这一方也被成为服务器。服务器从CLOSED
状态迁移到LISTEN
状态,也被叫做被动打开。
客户端通过主动打开的方式,向服务器发送一个SYN
同步报文,之后从CLOSED
状态迁移到SYN_SENT
状态。此时会启动一个定时器,如果在规定的时间内无法收到SYN+ACK
确认报文,那么客户端会自动从SYN_SENT
状态迁移到CLOSED
状态。
服务端收到SYN同步报文后,向客户端发送SYN+ACK
同步确认报文,从LISTEN
状态迁移到SYN_RCVD
状态,之后便等待客户端的ACK
确认报文。此时如果收到客户端的服务请求报文,便从SYN_RCVD
状态转移到LISTEN
状态。
客户端收到SYN+ACK
同步确认报文,向服务端返回ACK
确认报文,从SYN_SENT
状态迁移到ESTABLISHED
状态。如果此时收到应用层关闭连接的请求,便从SYN_SENT
状态迁移到CLOSED
状态。如果此时收到SYN同步报文的请求,便返回一个SYN+ACK
确认报文,从SYN_SENT
状态迁移到SYN_RCVD
状态。
服务端收到ACK
报文后,便从SYN_RCVD
状态迁移到ESTABLISHED
状态。如果此时收到应用层关闭连接的请求,便会发送FIN关闭连接请求报文,之后从SYN_RCVD
迁移到FIN_WAIT_1
状态。
客户端和服务端都进入到ESTABLISHED
状态,就可以随时进行数据通信,此时是平等独立的,任何一方都可以通过发送FIN
请求报文来关闭连接。
先发起FIN请求报文的一方A,从ESTABLISHED
状态迁移到FIN_WAIT_1
状态,等待对方ACK
确认报文或者FIN
报文。
对方B收到FIN请求报文后,返回ACK
确认报文,从ESTABLISHED
状态迁移到CLOSE_WAIT
状态,此时等待应用程序关闭连接(可能还有未发送完的数据)。
A收到B的ACK
确认报文,从FIN_WAIT_1
状态迁移到FIN_WAIT_2
状态,等待B的FIN请求报文。
B等待应用程序成功关闭连接后,发送FIN请求报文,从CLOSE_WAIT
状态迁移到LAST_ACK
状态,等待对方的ACK
确认报文。
A收到B的FIN请求报文,返回ACK
确认报文,从FIN_WAIT_2
状态迁移到TIME_WAIT
状态,并在2MSL
时间之后从TIME_WAIT
状态迁移到CLOSED
状态。
特别地,如果双方都主动的发起了关闭连接请求,那么双方此时都将处于CLOSING
状态,那么双方都只需要返回对应的ACK
确认报文后进入TIME_WAIT
状态即可。
4.数据传输
客户端和服务端使用TCP协议传输数据之前,需要通过三次握手机制建立连接,此时双方在内存中维护TCP连接的状态为“ESTABLISHED”。数据的收发采取“发送——确认”的模式,即:发送端发送数据包,接收端返回确认包。TCP报文中采用字节流组织数据,它将一个连接中的全部数据按照字节为单位进行编号,通信的双方各自维护着自己的数据字节编号,彼此独立。编号初始值的选取是随机的,因为报文中序列号的最大长度为32位,因此编号的取值范围为0~2^32-1。
假设发送方编号初始值为30,将要发送长度为5000字节的数据。按照上述的编号规则,这段数据的第一个字节为30,最后一个字节为5029,因为连接运载数据长度的限制,每次最多只能传输1024个字节,因为这段数据被分成5部分进行传输:
在报文中,长度为32位的序列号表示本机当前发送的报文中第一个数据在整个报文中字节序号。长度为32位确认序号表示本机期望收到对方下一个报文的数据字节序号,这个字段只有在ACK标志位置1时有效,也就说确认号只在确认报文中有效。
当发送端发送第一个数据报文后,接收端接收处理成功后返回一个确认报文(ACK位置1),并且确认号为1054。发送端收到确认报文后,继续发送第二个报文,接收端接收处理成功后返回确认号为2078的确认报文。后面以此类推,当然这样传输效率是非常低的,发送端每发送一个报文都需要等待接收端返回一个确认报文,因此,便有了滑动窗口机制。在每次的传输报文中,都会通过窗口大小来通知对方自己当前可以接收的数据大小,发送方可以根据窗口大小连续发送多个报文而不需要等待确认报文。当发送数据达到接收端窗口大小时,如果还未收到接收端的确认报文,则需要等待确认报文,如果在传输过程中收到接收端确认报文,则根据确认号调整自己的滑动窗口,以便继续发送数据。
TCP传输数据过程中通过窗口大小来说明接收方当前可以接收的数据量大小,因此TCP可以通过调整窗口大小来实现传输的流量控制。窗口大小主要由接收端来进行控制。当网络状况不佳,或者接收端处理负载过重时,减小窗口大小可以降低传输速度,当网络状况恢复或者接收端处理性能提升时,增加窗口大小可以提高传输速度。特别地,当窗口大小为0时,表示当前不能接收数据时,发送方就需要进行等待。具体等待多少时间呢?这就需要一个定时任务,每隔一定的时间进行一次检测,当接收方告诉自己可以接收数据时,也就是窗口大小大于0时,发送方根据窗口大小组织数据包,并立即发送。因此TCP坚持定时器就是用于定期检测接收方的窗口大小而设立的一种机制。
5.滑动窗口
滑动窗口的主要作用就是,允许发送方在收到对方响应之前,连续传输多个数据包。这样一来,接收方不需要等待对方为每一个数据包的响应,接收方也可以在收到多个数据包之后,只发送一个响应包即可,这就减少了不必要的数据传输,也加快整个传输过程。在数据发送端,所有的数据流都是按照顺序保存在发送缓存中。什么时候发送以及发送多少都是由滑动窗口决定的。
如下图所示,发送方在发送缓冲上维护一个发送窗口,窗口将整个缓冲区分为三个部分:窗口左边的部分表示已经发送并收到接收方确认的数据,窗口右边表示当前还不能发送的数据,窗口中间表示可以发送的数据。窗口中间分为两部分:4-5表示已经发送但未收到对方确认,7-9表示当前可以进行发送但未发送的数据。
此时,如果发送端收到接收端编号为6确认包,那么窗口向右滑动,如下如所示:
此时,发送端继续发送数据,直至可用窗口为0,假设此时仍未收到接收端的确认,那么发送端就不能再进行发送。如下图所示:
这里我们再详细说明一下滑动窗口:
①当发送端成功发送数据并收到了接收端的确认,滑动窗口的左边将会向右移动。
②当接收端的处理进程收到了数据,并释放了接收缓存空间,返回给发送端窗口大于0的确认包,那么发送端的窗口右边才会向右移动。如果接收端返回的确认包中窗口大小等于0,则说明当前接收端的接收缓存不能已满,等收到窗口大小大于0的窗口更新包之后,发送端的窗口才可以向右滑动。
6.超时和重传
发送端发送完数据后,必须要等待接收端的确认报文,以保证数据的可靠传输。在实际的传输过程,数据报文和确认报文都可能会出现丢失的情况,那么此时就需要一种超时和重传机制,在发送报文的时候设置定时时间,如果在超时之后,还未收到对方的确认报文,那么就需要重新发送之前的报文。超时和重传的关键之处在于策略的制定,如超时时间间隔,重传的频率。
TCP协议中为每个连接维护着四个定时器,它们分别是:①重传定时器。主要使用在发送端发送完数据报之后,等待接收确认报期间。②坚持定时器。主要使用在接收端窗口为0之后,周期性的探查窗口大小情况。③保活定时器。主要使用在服务器定期检测客户端是否存活。④2MSL定时器。主要使用在关闭TCP连接时。
发送端为每个发送出去的报文段设置一个超时定时器,当定时器溢出而报文的确认还没有返回时,就需要进行重传。那么在下一个确认数据报到来之前应该等待多长时间呢?这个时候我们需要知道数据段在双方之间往返一次的时间,如何测量这个时间?通过计算某报文段发送到接收到其对应的确认之间的时间间隔。
RTT(Round Trip Time)往返时间。路由和网络状况的变动,都会影响到RTT的计算,因此我们希望TCP协议能够跟踪这种变动情况,并及时更新RTT时间。
最初的TCP协议给出了一个计算公式:
R <-- α*R + ( 1 – α)*M
R:旧的RTT值。
α:平滑因子。
M:新测量的RTT值。
Jacobson提出了一种新的计算方法,RTO超时时间计算公式如下:
Err = M – A
A = A + g*Err
D D + h*( |Err| - D )
RTO = A + 4*D
M:本次测量的RTT值。
A:已测得的RTT平均值,初始值为0。
D:RTT估计的方差,初始值为6。
g:一般取1/8,便于计算,通过移位运算。
h:一般取1/4,便于计算,通过移位运算。
RTO:初始值取6,即3s。
这里还需要注意的是,如果多次重传都出现失败,如何更新RTO值呢?这就是接下来拥塞避免机制要处理的问题了。
7.慢启动与拥塞避免
在局域网中,通信双方通过指定窗口大小来控制着数传的速度,但是当通信双方中间存在多个路由器时,这样的机制就会出现一些问题。路由器要根据目的地址进行路由转发,转发之前需要对数据包进行缓存,这就造成了通信过程中的延迟,在通信量较大或网络状况不好时,因为延迟增加导致通信双方并不能及时知道对方的应答,之前我们提到的超时重传机制就会对之前的数据进行重发,这就加重路由器的缓存压力,而且还是一种恶性循环,因为网络状况不好导致更多数据重发,更多的数据重发导致更加恶化的网络状况,这种情况下,路由器很有可能因为内存空间原因导致崩溃,整个链路就会中断。因此为了应对这种由于网复杂性导致的延迟情况,提出了一种“慢启动”机制,由发送报文的一方主动减小发送速率。
(1)慢启动
具体做法是设置一个类似“发送窗口”的“拥塞窗口(CWND,Congestion Window)”,拥塞窗口大小初始值为MSS(最大数据段大小),待收到对方的确认后再逐渐增大。“慢启动”机制的具体步骤是:
①在一个TCP传输连接建立时,发送端将“拥塞窗口”初始化为MSS,即CWND = MSS。然后发送一个大小为MSS的数据段。
②如果在超时重传定时器溢出之前发送端收到了该数据段的确认,则发送端将“拥塞窗口”大小增加一个MSS,即CWND = 2MSS,然后发送2MSS大小的数据。
③同样地,在超时重传定时器溢出之前发送端收到了该数据段的确认,则将“拥塞窗口”大小增加两个MSS,即CWND = 4MSS,然后发送4MSS大小的数据。后面发送过程依此类推,每次都是在原来的基础上翻倍。
此外,“拥塞窗口”的增加也要上限,不能无限制增加,因此引入“慢启动阈值(Slow Start Threshold, SSTHRESH)”,其初始值为64KB。上述考虑的都是成功状态下的做法,当发生一次数据丢失时,SSTHRESH设置为当前的一半,而CWND又重新设置为1MSS,按照“慢启动”机制继续执行,当CWND再次增长到SSTHRESH便采用“拥塞避免”方案。
(2)拥塞避免
“拥塞窗口”不可能无限制增长,当CWND再次大于或等于SSTHRESH时,执行“拥塞避免”机制。其具体的做法是:
当CWND第二次大于SSTHRESH时,让“拥塞窗口”大小每经过一个RTT时间仅加1个MSS,此时CWND以线性增加。当再次发生数据丢失时,又会把SSTHRESH减为当前CWND的一半,同时把CWND置1,重新进入“慢启动”策略中。
8.快速重传与快速恢复
滑动窗口机制使得发送端不必在每次发送一个报文段之后等待确认,而可以发送多个报文段直至等于窗口大小,接收端可以在接收到多个报文段之后只返回最后一个报文段的确认即可。但是存在一个问题,如果在传输过程中,多个报文段中最后一个没有丢失,而中间的某个报文段丢失了,接收端要如何处理?假设现在发送端连续发出了M1、M2、M3、M4、M5五个报文段,发送过程中M2报文段出现了丢失,接收端没有收到M2,却收到了M4和M5,这样的话接收端需要通知发送端对从M2开始往后的M3、M4、M5都要进行重传,这样一来就会造成一些重复的操作,并增加了网络资源的浪费。因此需要提供一种机制——快速重传:当接收端收到M3之后,发现未收到M2,那么迅速通知发送端重新从M2开始发送,这样一来,如果发送端在未发出M4之前收到了接收端的通知,那么就会哦立即停止M4、M5数据的发送,并从M2开始重新进行发送。
快速重传的实现方法是:当接收端收到M3时,返回针对M1的确认,如果下一个报文段时M4,仍旧返回针对M1的确认,如果下一个报文段是M5,仍旧返回针对M1的确认,此时发送端连续收到3个针对M1的确认,就认为M2在传输过程中丢失,然后从M2开始重新进行数据的发送。
快速恢复的思想是:接收端在收到第三个重复确认后,把当前的CWND值设置为当前SSTHRESH值的一半,以减轻网络负担,然后执行“拥塞避免”策略,使CWND的值线性增长,以避免再次出现网络拥塞。
9.TCP 坚持定时器
滑动窗口机制使得通信的双方通过调整窗口的大小来控制传输流量,而在通信报文段的你来我往的过程中,相互都了解了彼此的窗口大小,但是有一种情况:即当接收端窗口大小为0时,发送端就必须止发送,这样的话,发送端就无法知道接收端何时能够接收数据,除非接收端发送一个非0窗口通告,但是这个通告丢失了会怎么样?发送端还是无法知道接收端是否可以接收数据。
因此,在发送端设置了一个坚持定时器,当定时器溢出时,发送端会发送一个“零窗口探查”报文段,当接收端收到此报文段后,如果可以接收数据,就返回一个包含窗口通告值的确认,如果不能接收数据,仍旧返回一个窗口为0的确认。当接收端收到一个非0窗口通告,则立即停止窗口探查。此外,对于每次发送“零窗口探查”报文段的时间间隔(即坚持定时器的溢出值)也是有要求的,每一次的时间间隔都是在前一次时间间隔的两倍。
注意:“零窗口探查”机制不同于“超时重传”机制之处在于,“零窗口探查”会一直坚持下去,而“超时重传”会在一定时间后重置连接。这是因为在“零窗口探查”中通信时正常的,只是对方一直回复窗口为0的确认,而在“超时重传”中多次重传都没有收到回复,表明连接已经出现了问题。
10.TCP 保活机制
之前我们讨论的都是客户端和服务器之间的数据传输,那么当TCP连接建立之后,即双方都处于ESTABLISHED的状态,如果几小时、几天设置是几个月、几年都没有进行数据的传输,那么服务器如何知道客户端是否在线?因此,在很多TCP/IP的实现中,加入了保活机制,即在服务端维护一个保活定时器,一般定时时间设置为2小时,当定时器溢出时,服务端会自动发送一个保活探查报文,此时客户端会有4种情况:
①客户端正常运行,且与服务端之间连接正常。当客户端收到探查报文后,返回一个确认,服务端收到确认后,重置保活定时器,如果在这之前有数据流动,都会将定时器重置。
②客户端已经崩溃,并且关闭或者正在重新启动。此时,客户端无法收到报文,也不能回复确认。而服务端会再发送9个探查报文,每个报文间隔75s,如果还未收到客户端响应,那么就认为可客户端已经关闭和终止此连接。
③客户端崩溃并已经重新启动。此时客户端会收到探查报文,但它会判断此连接无效,并向服务端返回一个复位报文。服务端收到复位报文后,关闭此连接。
④客户机正常运行,但是网络出现故障。处理步骤和②相同。