TCP/IP协议特点
- 面向连接:TCP协议在传输数据前需要建立连接,确保数据传输的可靠性。例如,网页浏览需要在浏览器和服务器之间建立TCP连接。
- 全双工通信:TCP连接建立后,双方可以同时发送和接收数据。例如,视频通话过程中,双方可以同时发送和接收音视频数据。
- 可靠性:TCP协议通过确认应答、重传机制和序号保证数据不丢失、无差错、不重复和按序到达。例如,文件传输需要确保文件完整性,TCP协议可以保证这一点。
- 面向字节流:TCP协议将数据视为连续的字节流进行传输。例如,下载文件时,TCP协议将文件数据视为连续的字节流,确保数据顺序接收。

报文段格式
- TCP虽面向字节流,但传输的数据单元是报文段
- 报文段=首部+数据

16位端口号:进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号.(比如DNS协议对应端口53,HTTP协议对应80,这些端口号可在/etc/services文件中找到)
32位序号(seq):TCP按字节编号.一次TCP通信中,发送方会先生成一个随机ISN.若发送一个字节流的第1025-2048字节,那么TCP报文段的序号seq就是ISN+1025.
32位确认号(ack):接收方用于对发送方的确认.若发送方发送了字节流的1025-2048字节,接收方全部接收成功,则发送一个对方ISN+2048+1的确认号ack,意思为告诉发送方下次发送请从字节流ISN+2048+1个字节处开始发送,前面的我已收到.
4位头部长度(header length):标识该TCP头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。
6位标志位包含如下几项:
- URG标志,表示紧急指针(urgent pointer)是否有效。
- ACK标志,表示确认号是否有效。我们称携带ACK标识的TCP报文段为确认报文段。
- PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区中)。
- RST标志,表示要求对方重新建立连接。我们称携带RST标志的TCP报文段为复位报文段。
- SYN标志,表示请求建立一个连接。我们称携带SYN标志的TCP报文段为同步报文段。
- FIN标志,表示通知对方本端要关闭连接了。我们称携带FIN标志的TCP报文段为结束报文段。
16位窗口大小(window size):是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
16位校验和(TCP check sum):由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。
16位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段seq的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。
TCP头部选项
TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节。典型的TCP头部选项结构如下图所示。

kind为选项的类型号,length为长度,第三个字段info则为具体信息.
常见的TCP选项有7种,如下所示.

kind=0是选项表结束选项。
kind=1是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
kind=2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segement Size,MSS)(TCP头部不计入)。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)字节。
MTU(Maximum Transmission Unit,最大传输单元)
以太网帧的MTU大小为46B-1500B,帧头部大小为14B-18B,帧尾部为4B的FCS(帧校验序列)
mac地址为6B,14B=2*mac地址+2B(ipv4/6的选择)
kind=3是窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子。在TCP的头部中,接收通告窗口大小时用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。假设TCP头部中的接收通告窗口大小是N*2M,或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修改/proc/sys/net/ipv4/tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。和 MSS 选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该 TCP 报文段的实际接收通告窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可参考标准文档 RFC 1323。
kind=4 是选择性确认(Selective Acknowledgment,SACK)选项。也就是kind=5的开关.TCP 通信时,如果某个 TCP 报文段丢失,则 TCP 模块会重传最后被确认的 TCP 报文段后续的所有报文段,这样原先已经正确传输的 TCP 报文段也可能重复发送,从而降低了 TCP 性能。SACK 技术正是为改善这种情况而产生的,它使 TCP 模块只重发丢失的 TCP 报文段,不用发送所有未被确认的 TCP 报文段。选择性确认选项用在连接初始化时,表示是否支持 SACK 技术。我们可以通过修改 /proc/sys/net/ipv4/tcp_sack 内核变量来启用或关闭选择性确认选项。
kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
kind=8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。
建立连接过程
建立连接前客户端、服务器都处于关闭状态(CLOSED),直到客户端主动打开连接,服务器才被动打开连接(处于监听状态LISTEN),等待接受客户端的请求。
1、第一次握手:将同步标志位设为1(SYN=1),随机选择一个序号(seq=x)。客户端进入SYN_SEND状态。
2、第二次握手:服务器收到请求连接报文段后,若同意建立连接,则将同步标志位设为1(SYN=1),确认标记为设为1(ACK=1),随机选择一个序号(seq=y),确认收到的序号(ack=x+1),服务器进入同步已接收状态SYN_RCVD。
3、第三次握手:客户端收到确认报文段后,向服务器再次发出连接确认报文段,确认标记位设为1(ACK=1),序号(seq=x+1),确认收到的序号(ack=y+1)。客户端先进入ESTABLISHED状态,服务端收到后同时进入。

为什么建立连接需要三次握手,两次行不行?
三次握手是为了确定客户端、服务器收发数据都正常。防止服务器接收了早已失效的连接请求,从而一直等待客户端请求,最终导致浪费资源。如果只有两次握手,就会出现,客户端发出的第一个连接请求段没有丢失,只是在某个网络节点长时间滞留,而客户端进入了超时重传,重新发送了一个连接请求,建立连接,传输完数据之后连接释放,而此时第一次发送的连接请求到达服务器,这是一个失效的连接请求,但是服务器不知道,正常确认连接,此时客户端已经关闭,服务器一直等待。所以两次不行。
- 第一次握手:客户端发送请求,此时服务器知道客户端发送数据正常。
- 第二次握手:服务器发送确认,此时客户端知道服务器接收、发送数据都正常。
- 第三次握手:客户端发送确认,此时服务器知道客户端接收、发送数据都正常。
三次握手:确认双方收发能力正常.
如果已经建立连接,但是客户端突然出现故障怎么办?
TCP还设有一个保活计时器,Client端如果出现故障,Server端不能一直等下去,这样会浪费系统资源。每收到一次Client客户端的数据帧后,Server端都的保活计时器会复位。计时器的超时时间通常是设置为2小时,若2小时还没有收到Client端的任何数据帧,Server端就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,Server端就认为Client端出了故障,接着就关闭连接。
释放连接过程
- 第一次挥手:客户端(服务器也可以主动发,一般是客户端)向服务器发送一个断开连接请求报文,FIN=1、seq=u。发送完成后,进入FIN_WAIT-1(终止等待1)状态。这表示客户端没有业务数据要发给对方了,通知服务器自己要断开连接了。
- 第二次挥手:正常情况下,在收到客户端发送的FIN断开连接请求之后,服务器都会发送一个ACK响应报文,ACK=1、seq=v、ack=u+1。该报文的意思是,我同意你的断开连接请求,服务器进入CLOSEWAIT(关闭等待)状态,此时TCP协议服务会通知高层的应用进程,对方已经没有数据要发送了,如果我方还有数据要发送,可以继续发。客户端进入FIN_WAIT-2(终止等待状态2)。
- 第三次握手:在发送完ACK确认报文后,服务器还能继续发送业务数据,当服务器发送完数据后,或者CLOSEWAIT(关闭等待)截止后,服务器会主动发送断开连接请求,FIN=1、ACK=1、seq=w、ack=u+1。表示服务器也没有数据要发送了,然后进入LAST_ACK(最后确认)状态。
- 第四次挥手:客户端在收到服务器的断开连接请求报文后,进行最后的确认,向服务器发送一个ACK确认报文,然后进入TIME_WAIT(超时等待状态),在等待2MSL(Maximum Segment Lifetime,最大报文生存时间)*2的时间后,如果期间没有收到其他报文,则证明对方已正常关闭,主动断开方的连接最终关闭。

最后为什么要等待2MSL之后才关闭
1. 保证最后丢失的ACK能被重传
2. 防止旧连接报文污染新连接
1. 2MSL等待时间的第一个核心目的:确保连接的可靠关闭
因为最后一次的确认报文有可能丢失,如果服务器超时等待没有收到确认报文,会多发几次,此时如果客户端直接断开连接,那服务器一直没得到响应,会一直发,浪费资源。如果直到2MSL,主动断开方都没有再一次收到对方的报文(如FIN报文),则可以推断ACK已经被对方成功接收,此时,主动断开方将最终结束自己的TCP连接。
一个报文最多会用1MSL的时间到达目的地,如果超过了1MSL也就自动消亡了
- 极端情况1:客户端–ACK–>服务器,花费1MSL到达, 服务器关闭.客户端再等1MSL后关闭,因为服务器重发–FIN–>客户端最多需要1MSL.若服务器发现ACK丢失,再次发送FIN也可以在客户端TIME_WAIT期间送达,从而让客户端的再次重发ACK.
- 极端情况2:客户端接收到服务器的FIN报文后,向服务器发送ACK报文丢失了.服务器没收到ACK报文后会重复发送FIN报文(服务器一般不会等满1MSL再重发FIN报文),会发送多次,但是都丢失了.即使这样服务器在重复多次发送后,到达TCP的FIN重传次数限制后服务器也会自动关闭连接. 客户端在2MSL时间后没有收到新的FIN报文后也自动关闭.
2. 2MSL等待时间的第二个核心目的:防止“旧连接”的数据包干扰“新连接”
网络的复杂性意味着数据包的传输并非总是按序、准时到达的。一个数据包可能因为路由选择、网络拥塞等原因在网络中“迷失”一段时间,然后才到达目的地。这就引出了2MSL等待时间的第二个重要作用:防止已失效的旧连接数据包(也称为“迷途报文”)干扰新的连接 。
一个TCP连接(由源IP、源端口、目标IP、目标端口组成的四元组唯一标识),四元组被释放后可以立刻重用.
想象一个场景:如果没有TIME_WAIT状态.一个刚刚建立的连接,因为四元组与旧连接一致,接收到残留在网络中的旧FIN报文后,立刻关闭.
TIME_WAIT状态通过强制等待2MSL,有效地解决了这个问题。MSL定义了一个报文在网络中的最长生命周期。因此,等待2MSL可以确保:在一个连接正常关闭后,所有与该连接相关的、在网络中传输的数据包,都已经在2MSL时间内自然消亡了 。当2MSL时间结束后,任何后续使用相同四元组建立的新连接,都不会再受到上一个旧连接残留数据包的干扰,保证了新连接的纯净性。
注:TCP的设计假设中,2MSL时间内所有旧报文都会消亡,MSL值也只是假设值,tcp不会去测量出来,不必钻牛角尖.即使真的有极端情况,旧FIN报文活到了新连接开始后到达,还有其他保护机制(序号比对,随机ISN等)保证不被旧报文干扰.
例:发送方旧FIN报文seq=100,接收方新连接随机ISN=10000000,新seq=ISN+x;接收方接收窗口发现seq:100不在其中,直接丢弃.
为什么关闭连接需要四次挥手,而建立连接只需要三次握手?
关闭连接时,被断开放在接收到对方的FIN断开连接请求报文时,很可能还有业务数据没发送完成,并不能马上断开连接,又不能对对方的断开连接请求置之不理,因为对方没有收到确认会认为断开连接请求在路上丢失,触发超时重传。所以只能先回复一个ACK确认报文,告诉对方,我知道你要断开连接了,但我有业务数据要发送,只能等待我所有的业务数据都发送完才能真正结束,在结束之后,我会给你发送FIN+ACK断开连接请求报文的。所以被断开方的确认报文需要分成两步,故需要四次挥手。
在建立连接的时候,只需要确认双方都能收发数据。
复位报文段(RST)
在某些特定的场合,TCP连接的一段会向另一端发送TCP头部信息携带RST标志的报文段,即复位报文段,以通知对方关闭连接或者重新建立连接。
1.访问不存在的端口
对于UDP协议,当一个数据报到达目的端口时,该端口没被监听使用,它将产生一个ICMP端口不可达的信息;对于TCP,目的主机将会产生一个复位报文段返回.
因为复位报文段的接收通告窗口大小为0,即收到复位报文段的一段应该关闭连接或者重新连接,而不可回复此复位报文段。
另外,当客户端程序向服务端的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占用,客户端程序也会收到复位报文段。
2.异常终止连接
TCP协议提供了异常终止一个连接的方法,即给对方发送一个复位报文段,一旦发送了该报文,发送端所有排队等待发送的数据都将被丢弃,接收端将关闭或者重新建立连接。应用程序可以设置socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。
3.处理半打开连接
半连接状态:客户端(服务端)关闭或者异常终止了连接,而对方没有收到结束报文段(可能发生网络故障),此时服务端(客户端)还维持原来的连接,而客户端(服务端)即使重启,也已没有该连接的任何信息。
如果客户端(或服务器)往处于半打开状态的连接写数据,则对方也会回复一个复位报文段。
交互数据流和成块数据流
交互数据流一般用于传输命令之类的小数据,请求更频繁,更注重时延优先
成块数据流一般用于传输大文件,请求不频繁,一般是写满一个MSS才发一次,更注重吞吐量
| 对比项 | 成块数据流(Bulk Data) | 交互数据流(Interactive Data) |
| 典型场景 | FTP传文件、HTTP下载、网盘下载 | Telnet、SSH、远程终端、数据库命令 |
| 数据量 | 大量连续数据 | 少量零散数据 |
| 数据产生方式 | 应用一次产生很多数据 | 应用频繁产生少量数据 |
| TCP发送特点 | 尽量填满发送窗口连续发送 | 经常发送小报文 |
| 关注重点 | 吞吐量(Throughput) | 时延(Latency) |
| 用户感知 | 希望下载更快 | 希望响应更快 |
| 报文大小 | 通常接近 MSS | 通常远小于 MSS |
| 滑动窗口利用率 | 高 | 低 |
| ACK作用 | 保证连续传输 | 保证及时响应 |
| Nagle算法效果 | 影响较小 | 影响明显 |
| 典型现象 | 一次发送大量 TCP 段 | 经常出现小包(Tinygram) |
| 优化方向 | 增大窗口、提高带宽利用率 | 减少延迟、减少等待时间 |
延时确认:服务端(客户端)发送确认报文段时不会立即发送,通常会把自己要发的数据打包进这个确认报文段一并发送出去.tcp握手的第三次握手和挥手的第二次挥手,也可能发生延迟确认.
具体表现为:
- 第三次握手的时候,把自己数据也带过去了,数据和ack一起发送;
- 第二次挥手的时候,也带上了余下需要发送的所有数据,数据和FIN与ack一起发送,就没有了CLOSE_WAIT阶段.
Nagle算法:
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之后,才被允许发送下一个TCP报文段.
发送方在等待对方的确认报文段到达之前,可以不停收集本端需要发送的微量数据,等确认到达以后一并发出.
该算法可以大大减少网络上微小TCP报文段的数量.
相当于收一次发一次,网络越好,确认到达得越快,数据发送得也越快.
带外数据
URG标志位置1时,序号+紧急指针相加的值会指向紧急数据的最后的字节的下一个字节.
例:seq=4,紧急指针为3,那么带外数据在字节流的下一个位置是7,那么带外数据的位置就是6.
一段紧急数据里面也只有最后一个字符会被当成真正的带外数据放进带外缓存(需要即时读取),其他数据会被视为是普通数据.
极少使用,鸡肋功能,也就表示下这个报文段是带紧急数据的报文段
实现可靠传输
1、滑动窗口
发送窗口:发送方维护的一段连续的、允许发送的数据字节序号范围。发送窗口中的数据可分为四部分:已发送并收到确认的数据、已发送但尚未收到确认的数据、允许发送但尚未发送的数据、暂时不允许发送的数据。发送窗口主要控制发送方能够发送的数据量。当收到接收方的ACK确认后,发送窗口向前滑动,将原本不允许发送的数据纳入发送窗口。发送窗口的实际大小由接收窗口(rwnd)和拥塞窗口(cwnd)共同决定,即:
发送窗口 = min(rwnd, cwnd)
其中流量控制由接收窗口实现,拥塞控制由拥塞窗口实现。

接收窗口:接收方维护的一段连续的、允许接收的数据字节序号范围,用于控制发送方的发送速率。收到符合条件的数据后,接收窗口向前滑动,并通过ACK通知发送方;超出接收窗口范围的数据通常会被丢弃。

2、重传机制
超时重传:重传超时时间(RTO)
每当发送一个数据包时,发送方会启动一个超时计时器。如果在规定时间内没有收到接收方的确认应答(ACK),则认为该数据包丢失,需要重新发送。
若出现RTO超时,那么TCP就认为网络出现了严重拥塞,拥塞控制直接转入慢开始阶段
快速重传:
如果发送方连续收到三个重复确认应答,则认为该数据包可能丢失,不必等待超时计时器到期,立即重传该数据包。
3、流量控制
接收方根据自己接收缓存的大小(rwnd),动态调整发送方发送窗口的大小(swnd),从而控制发送方的发送速率,避免出现发送方和接收方速度不匹配的问题。即发送方发送的太快,接收方来不及接收,发送方触发超时重传,恶性循环。
注:最终的swnd(发送窗口)取决于rwnd(接收窗口)和cwnd(拥塞窗口)中的较小者

从上图可知接收方进行了两次流量控制,第一次把窗口减少到rwnd=300,第二次直接减少到了0,即不允许发送方再发送数据了。
4、拥塞控制
实时进行网络拥塞检测和拥塞窗口调整,防止过多的数据注入到网络中,防止通信网络链路过载的同时,提高网络吞吐量.
1.慢开始:在新连接建立或网络拥塞(RTO超时,TCP认为是严重拥塞)发生后的初始阶段,通过逐步增加拥塞窗口(cwnd)的大小来探测网络容量,避免网络突然过载。

- 初始阶段:拥塞窗口(cwnd)设置为一个较小的值(通常为1个最大报文段,MSS)。
- 指数增长:每次收到一个ACK,cwnd增加1个MSS。这样,cwnd呈指数增长。
- 门限值:当cwnd达到一个门限值(ssthresh)时,慢启动阶段结束,进入拥塞避免阶段。
2.拥塞避免:拥塞避免算法在慢开始后期或网络已趋于稳定时,通过线性增长cwnd来防止网络拥塞。
- 线性增长:每经过一个RTT(往返时间),cwnd增加一个MSS。
- 拥塞检测:TCP会不断观察各种信号(丢包,RTO超时等)状态来推断拥塞情况,如果TCP认为发生了网络拥塞,ssthresh被设置为当前cwnd的一半,进入慢启动(触发了超时重传)或快速恢复(触发了快速重传)阶段。
3.快重传:接收方每收到一个失序的报文段(顺序对不上或者不在接收窗口内)后 就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时才进行捎带确认。
发送方只要一连收到3个重复确认就立即重传对方尚未收到的报文段(TCP认为是出现了轻度拥塞),而不必继续等待重传计时器到期,提升了网络性能。
4.快恢复:快速恢复算法在快速重传后立即调整拥塞窗口,使得网络迅速恢复到稳定状态。
linux中快恢复顺序如下:
1.若收到3个重复的确认报文段时,TCP就认为是拥塞发生了.
此时会令:ssthresh=ssthresh/2; cwnd=ssthresh+3*MSS;
2.若再次收到重复的确认报文段,就增加拥塞窗口大小
cwnd=cwnd+MSS;
3.若收到了新数据的确认报文段,就把拥塞窗口设置为门限值
cwnd=ssthresh;
能收到重复的ACK确认报文段,就说明发生了部分丢包(轻度拥塞),不过网络还没严重拥塞到需要慢开始,减半后可以继续加cwnd大小。等快恢复结束后拥塞控制就会回到拥塞避免阶段.

拥塞控制的状态转移:
慢开始:新建立连接或者网络拥塞(RTO超时)时进入;
拥塞避免:cwnd到达ssthresh时进入,同时开启拥塞检测;
快重传和快恢复:接收到三个重复ACK后进入,并且快速恢复cwnd到合适大小;
超时重传:RTO出现超时,重传对应报文段,直接进入慢开始阶段.
教科书简化版: cwnd=ssthresh=ssthresh/2;