TCP连接的主要特点:
- TCP协议是面向连接的运输层协议,在使用TCP协议前必须建立TCP连接,数据传输完后必须释放连接。
- TCP连接提供可靠交付服务,通过TCP连接传送的数据,无差错、不丢失、不重复且按序到达。
- TCP连接提供全双工通信,即允许通信双方在任何时候都能发送数据。连接两端都设有发送缓存和接收缓存,用来存放双向通信的数据。上层应用只需往缓存里面写入和和读取数据。
- TCP连接只能由是点对点(一对一),不行UDP协议一个主机可以同时向多个主机发送消息。
- TCP面向字节流传输,意思是虽然上层应用程序和TCP交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据看成一连串的无结构字符流(TCP并不关心应用程序一次把多长的报文发送到TCP缓存,而是根据当前的窗口值和当前网络拥塞程度来决定一个TCP报文应该有多少字节,例如发送方应用程序交给发送方TCP程序10个数据块,但接受方TCP只用了4个数据块就把字节流交付到上层应用),TCP要确保发送方和接收方的字节流必须完全一样。
1.1 TCP报文格式

源端口:发送方端口号
目的端口:接受方端口号
序号(Sequence Numbers):占4个字节(0-2^32-1,4294967296个序号),序号超过该范围后又从0开始。TCP连接中每个字节都按顺序编号,例如一个报文的序号是301,该报文携带的数据为100字节,最后一个字节的序号为400,则下个TCP报文的序号为401。
确认号(Acknowledgment Number):期望收到对方下一个报文段的第一个字节的序号。若确认号为N,则表示序号到N-1为止的所有数据都已正确接收。
数据偏移:(也叫首部长度),TCP报文段的数据起始位置距离TCP报文段的起始处有多远,即TCP首部长度。注意”数据偏移的单位是4字节”,即4位最多能表示十进制数为15,首部长度最多为4×15=60字节,选项长度最多为40字节。
保留:保留为日后使用,目前全置为0。
URG:当URG=1时,告诉系统文段中有紧急数据,应当优先处理。例如当URG置一时,应用程序告诉TCP有紧急数据传送,发送方TCP配合紧急指针把紧急数据插到本报文数据段的最前面,而在紧急数据后面的数据仍是普通数据。
ACK:TCP规定,在建立连接后所有报文必须把ACK置1。
PSH:接收方收到PSH=1的报文,应尽快交付给应用进程,而不用等到整个缓存都填满了再向上交付。
RST:当RST=1时,表示TCP连接出现错误,必须释放连接,重新建立连接。
SYN:在连接建立时用来同步同步序号,当SYN=1,而ACK=0,表示连接请求报文,若对方同意建立连接,则在相应报文中是SYN=1,ACK=1。因此SYN=1就表示这是一个连接请求或连接接受报文。
FIN:当FIN=1时,表示此报文段发送方的数据以发送完毕,并要求释放连接。
窗口:占两个字节,值是[0, 2^16-1],窗口指的是发送本报文段的一方的接受窗口,窗口值告诉对方,从本报文段首部中的确认号算起,目前允许对方发送的数据量。
校验和:检验TCP数据报在传输过程中有无出错。
紧急指针:在URG=1时才有意义,指出本报文段中的紧急数据的末尾在报文段中的位置。
1.2 TCP连接建立与释放
建立连接 三次握手

第一次:建立连接时,客户端把SYN置为1,发送SYN包(随机生成序号seq=x)到服务器,并进入SYN_SENT状态,等待服务器确认,该报文不含应用层数据。
第二次:服务器收到SYN包,必须发送一个ACK确认包确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(随机生成序号seq=y),这里的SYN报文和ACK报文可以合并到一起发送,把SYN和ACK置为1,该报文也不含应用层数据。
第三次:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。该报文可以携带客户端应用层数据给服务端。服务端收到ACK报文也进入ESTABLISHED状态。
握手过程中操作系统内核过程
内核会对某个端口进行监听,当该端口收到一个SYN报文时会把该报文放入一个由操作系统维护的SYN队列中,把状态由LISTEN状态变为SYN_RECEIVED状态,并发送SYN报文和ACK报文,而此时应用程序处于accept阻塞状态。当收到对方传来的ACK报文时,把第一次收到的SYN报文从SYN队列中出队,进入到ACCEPT队列,应用程序从ACCEPT队列中拿到连接,从而开始进行通信。
释放连接 四次挥手

第一次:客户端进程发出连接释放FIN报文,并且停止发送数据。FIN=1,seq=i(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1状态。
第二次:服务器收到连接释放报文,发出确认报文,ACK=1,ack=i+1,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态,客户端收到报文则进入FIN-WAIT-2状态。TCP服务器通知高层的应用进程,客户端向服务器的连接要释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
第三次:服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=j,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次:客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=j+1,此时,客户端就进入了TIME-WAIT状态。服务器只要收到了客户端发出的确认,立即撤销相应的TCB进入CLOSED状态。注意此时客户端还没有进入CLOSED状态,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
常见面试点
【问题1】为什么客户端收到服务端FIN报文后先进入TIME_WAIT状态,等待2MSL时间才能进入CLOSED状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。其次,由于网络问题旧的数据在关闭连接前没有到达客户端,而在相同的端口被另一个TCP连接复用后才到达,而此时的客户端是新的客户端,这是就会产生数据错乱。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题2】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET(还有数据未发送),所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题3】为什么不用两次、四次握手进行连接?
答:TCP 设计中一个基本设定就是,通过TCP 连接发送的每一个包,都有一个唯一的sequence number,所以接收方可以根据序号按序接收,而且可以去除重复的报文。
① TCP连接是双方的,对于通信双方A、B来讲,如果只进行两次握手,A发送SYN报文,B收到报文后返回一个SYN报文和ACK报文,对于B来讲就确认连接建立了,但网络通道是不可靠的,此时A可能没收到ACK报文不知道B是否准备好,就会一直等待ACK报文并重发SYN报文,没法保证双方初始序号都能被确认接收。
②对B来讲,由于网络原因可能会收到多个SYN报文,但两次握手没法判断哪个SYN报文是否过期,有可能收到的是历史连接请求,这就会造成历史连接初始化,同时没办法保证ACK报文是否被接收,因此B每收到一个SYN报文就建立一次连接,造成资源浪费。而采用三次握手A收到一个ACK报文后发现是一个历史连接,可以发送一个RST报文要求终止历史连接。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?
答:TCP还设有一个保活计时器(keep alive),显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。另外还需要考虑以下情况:
1.当对端程序崩溃并重启,对端在探测报文发送前重启了,是可以响应探测报文,但由于没有该连接的有效信息,本机会发出一个RST报文要求重新连接。
2.当对端程序崩溃并没有重启,探测报文没有得到响应,连续几次后,宣布该TCP连接死亡。
1.3 MTU和MSS

MTU:一个网络包最大的数据长度
MSS:除去IP首部和TCP首部后数据长度
为什么需要TCP协议要设置一个MSS?
在不考虑MSS时,TCP报文传送到网络层时,经过封装后一个网络包超过MTU时,IP层就要进行分片。当某一个分片的IP报文丢失,由于IP层没有重传机制,当TCP发现数据缺少,不会返回ACK报文给对方,那么发送方TCP需要重新发送整个报文。而在双方建立连接时协商好MSS的大小,当TCP报文大小超过MSS时就会进行分片,提交给IP层的数据大小自然不会超过MTU,从而不会在IP层进行分片。如果分片数据在发送过程中出现丢失,只需要重新发送丢失的分片,不需要重新发送整个TCP报文。
1.4 TCP的可靠性传输
为了实现可靠性传输,就要解决数据丢包、重复、乱序等问题。TCP可通过重传机制、流量控制、拥塞控制等方案解决上述问题
1.4.1重传机制
超时重传

发送方在发送数据时设定一个定时器,当超过时间后没有收到对方的ACK报文(可能是M1报文丢失或者ACK报文丢失),则会重新发送数据。
如何设置超时时间呢?
如果超时时间过长,丢包大半天才发现,效率低,性能差。如果超时时间过短,可能没有丢包就重发,增加网络拥塞,导致更多超时,从而有导致更多重发。
TCP采用自适应算法,动态修改重传时间,加权平均往返时间(RTTs),即部分已收到确认的报文平均RTT。
快速重传

不同于超时重传,快速重传并不以时间为驱动重传。发送方可以不用等待ACK报文就可以发送下一个报文。TCP采取累计确认方式,即每发送一个ACK时都会指明下一个期待的序列号,如果该序列号一直没有收到,则会一直重传该ACK报文。
例如:
当接收方收到了Seq1的报文后,返回一个ACK2报文,由于网络问题Seq2报文没有到达,但Seq3、Seq4、Seq5到达了,此时接收方还是发送ACK2报文,因为Seq2还未收到。当发送方收到多个ACK2报文,就知道Seq2还没有发送成功,就会重传Seq2的报文。接收方收到Seq2报文后,由于Seq3、Seq4、Seq5都收到了,所以发送ACK6报文。但是发送方连续收到多个ACK2报文,是只重传ACK2报文还是重传ACK2及后面的报文呢?这两种情况都是有可能的。
选择性确认(SACK)

接收方返回的ACK报文里面可以将缓存地图发给发送方,这样发送方就知道哪些数据收到了,哪些数据没有收到,只需要发送缺少的报文段。
1.4.2 滑动窗口
重传机制中的快速重传和选择性重传其实是基于滑动窗口实现的。就像聊天一样,如果我发送一句话都要等待你的回应才能发送下一句话,无疑效率是非常低下的。而引入了滑动窗口。窗口其实是在内存中开辟一个缓存空间来存放数据,分为接收窗口和发送窗口。而TCP首部的窗口值是告诉对方自己的接收窗口还能接收多少数据。
发送方知道接收方的接收窗口大小后确定发送窗口大小,无需等待接收方的ACK报文,可以继续发送数据直到当前窗口的最大值。发送方在等待接收方的ACK报文前,必须保留发送数据,等收到了接收方的ACK报文后,向前滑动窗口,清除已发送的窗口外的数据。

当发送方把所有数据一下子发送出去,而没有收到ACK,这下可用窗口大小就为零,这时不能再发送数据,只能收到ACK确认报文后进行窗口滑动才能发送数据。
注意,要进行窗口滑动时,所滑动的窗口只能是小于当前收到最大ACK值的窗口。接收方发送ACK报文意味着ACK值之前的报文都已经收到。例如发送方直接收了ACK37和ACK39,这时接收方意味着39之前的报文都已经接收,所以并不用重新发送ACK38,这个模式叫做累计确认。(如果接收方一直收不到ACK38的值,又回到我们上面的问题,发送方会重复发送ACK38的回应报文,直到收到Seq=38的报文)
1.4.3 流量控制
发送方不能一直不断地给接收方发送数据,要考虑接收方的数据处理能力,无脑地发送数据导致接收方处理不过来,会触发重传机制,又会使对方继续处理不过来,形成恶性循环。TCP提供了一种可以让 [发送方] 根据 [接收方] 的实际接收能力控制发送的数据量,即流量控制,流量控制也是基于滑动窗口实现的。
上面说过,窗口中的数据都是存放在内存缓冲区中的,但是缓冲区的大小可以被操作系统进行调整。当系统资源非常紧张时,操作系统可能会直接减少接收窗口缓冲区的大小,当应用程序无法及时读取缓冲区数据,这是就会出现数据丢失的现象。
为了防止该情况出现,TCP采用先收缩窗口,再减少缓冲区大小的方法,避免发生数据丢失。当窗口大小变为0时,也就是窗口关闭,接收方就会拒绝接受发送方的数据。
在接收方窗口由0变为非0时,会发送一个ACK报文告知对方,但是如果这个报文丢失了,就会出现死锁现象:
为了解决这个问题,TCP设有一个定时器,只要TCP连接一方收到对方窗口为0的ACK报文,就会启动计时器,在计时器期间没有收到窗口非0的ACK报文,就会发送一个窗口探测报文,对方接到这个报文会给出自己现在的接收窗口大小。如果接收窗口仍为0,则重新启动计时器,等待下次再次发送窗口探测报文,若超过探测次数,则会发送RST报文终止本次连接。
1.4.4 拥塞控制
拥塞窗口
拥塞窗口(cwnd)是发送方维护的一个状态量,只要网络出现拥塞,拥塞窗口的值就会变小,网络没有出现拥塞,拥塞窗口的值就会变大。当发生了超时重传就会认为当前网络拥塞,此时发送窗口的值应为 MIN( 对方接受窗口,拥塞窗口)。
慢启动算法
慢启动的意思是一点一点提高发送的数据量,当发送方每收到一个ACK报文,拥塞窗口大小加1。刚建立连接只能发送一个数据包,收到一个ACK报文后,可以一次发送两个数据报文,呈指数上升。
避免拥塞算法
由于慢启动呈指数增加,所以到达一定阈值后就不能继续增加了,这个阈值叫做慢启动门限(ssthresh),当超过该阈值就会采用避免拥塞算法,每收到一个ACK报文后,拥塞窗口增加 1/cwnd。
快速恢复
当拥塞发生时,就会触发重传,有超时重传和快速重传
若采用超时重传,把ssthresh设为 cwnd/2,然后cwnd置为1,然后重新开始慢启动,传输效率大幅度下降。
采用快速重传,会把 cwnd置为 cwnd/2,ssthresh = cwnd,然后采用快速恢复算法,cwnd = ssthresh + n (n表示最近收到的ACK报文数量),可以开始重传数据包。若收到重复的ACK则cwnd = cwnd + 1;若收到新的ACK,说明网络开始恢复,则cwnd设置为采用快速恢复算法前ssthresh的值,即拥塞避免状态。

