- UDP
- TCP[💖💖💖]
- 传输层常见问题
- 1. MSS(Maximum Segment Size,最大报文长度)设置地太大或者太小会有什么影响?
- 2.为什么IP层会分片,TCP还要分段
- 3. 为什么TCP采用三次握手建立连接,而不是不采用两次或四次握手建立连接?[💖]
- 4. 三次握手的过程中,可以携带数据吗💖
- 5. 半连接队列与SYN Flood攻击💖
- 6. 为什么建立连接是三次握手,关闭连接是四次挥手?
- 7.TCP快速打开原理(TFO)
- 8. TCP四次挥手详解【💖】
- 9. 四次握手
- 10.Nagle算法与延迟确认
- 11.TCP粘包问题【💖】
- 12. TCP Keep-Alive与HTTP Keep-Alive
- 13.假设互联网中所有链路的传输都不出差错,所有结点都不发生故障,这种情况下TCP的”可靠交付”功能是否多余?
网络层提供主机之间的逻辑通信,传输层提供不同主机上进程之间的逻辑通信。
UDP
UDP数据报格式

- 源端口:在需要对方回信时选用,不需要时可全0
- 目的端口: 目的端口号,这在终点交付报文时必须使用,不然数据交给谁呢?
- 长度: UDP的长度,最小值为8字节,仅有首部
- 检验和: 检测用户数据报在传输过程是否有错,有错就丢弃。
IP数据报和UDP数据包的区别:IP数据报在网络层要经过路由存储转发;而UDP数据报是在传输层的端到端的逻辑信道中传输,封装成IP数据报在网络层传输时,UDP数据报的信息对路由不可见。
UDP协议的特点:
- UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务。
- 传输途中出现丢包,UDP 也不负责重发。
- 当包的到达顺序出现乱序时,UDP没有纠正的功能。
- 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为。
- 如果需要以上的细节控制,不得不交由采用 UDP 的应用程序去处理。
- UDP 常用于以下几个方面:
- 1.包总量较少的通信(DNS、SNMP等);
- 2.视频、音频等多媒体通信(即时通信);
- 3.限定于 LAN 等特定网络中的应用通信;
- 4.广播通信(广播、多播)。
TCP与UDP的区别对比
TCP[💖💖💖]
TCP和网络层虚电路的区别
- TCP报文段在传输层抽象的逻辑信道中传输,对路由器不可见;
- 虚电路所经过的交换节点都必须保存虚电路的状态信息。
在网络层若采用虚电路方式,则无法提供无连接服务;而传输层采用TCP协议不影响网络层提供无连接服务。
TCP的特点
TCP是面向连接的协议;
- 每一条TCP连接只能有两个端点,每条TCP连接是点对点的;
- TCP提供可靠的交付服务,保证传输的数据无差错、不丢失、不重复且有序;
- TCP提供全双工通信,TCP允许通信双方应用进程任何时候都能进行发送和接收数据,所以TCP连接的两端都设有接收和发送缓存;
- TCP是面向字节流的。
TCP报文头部格式

- 来源端口(16位长):识别发送连接端口;目的端口(16位长):识别接收连接端口
TCP 连接的四元组——源 IP、源端口、目标 IP 和目标端口,唯一标识一个连接。 由于IP 层就已经处理了 IP地址 ,所以TCP 只需要记录两者的端口即可。 - 序列号(seq)是本报文段第一个字节的序列号,32位长无符号整数,达到最大值就循环到0。
使用序列号的作用:①在SYN报文中交换彼此初始序列号;②保证数据报按正确顺序组织。
ISN:Initial Sequence Number,初始序列号。在三次握手的过程当中,双方会用SYN报文来交换彼此的ISN。
ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0,这个算法使得猜测 ISN 变得很困难。如果 ISN 被攻击者预测到,要知道源 IP 和源端口号都是很容易伪造的,当攻击者猜测 ISN 之后,直接伪造一个 RST 后,就可以强制连接关闭的,这是非常危险的。而动态增长的 ISN 大大提高了猜测 ISN 的难度。 - 确认号(ack,32位长):期望收到的数据的开始序列号。也即已经收到的数据的字节长度加1。
- 数据偏移(4位长):以4字节为单位计算出的数据段开始地址的偏移值。
- 保留(3比特长):须置0
- 标志符(9比特长)
- NS—ECN-nonce。ECN显式拥塞通知(Explicit Congestion Notification)是对TCP的扩展,定义于 RFC 3540 (2003)。ECN允许拥塞控制的端对端通知而避免丢包。ECN为一项可选功能,如果底层网络设施支持,则可能被启用ECN的两个端点使用。在ECN成功协商的情况下,ECN感知路由器可以在IP头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样。
- CWR:Congestion Window Reduced,定义于 RFC 3168(2001)。
- ECE:ECN-Echo有两种意思,取决于SYN标志的值,定义于 RFC 3168(2001)。
- URG:为1表示高优先级数据包,紧急指针字段有效。
- ACK:为1表示确认号字段有效。
- PSH:为1表示是带有PUSH标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
- RST:为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN:为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步。
SYN=1,ACK=0表示这是一个连接请求报文;如果对方同意建立连接,则在响应报文中使用SYN=1,ACK=1。 - FIN:为1表示发送方没有数据要传输了,要求释放连接。
- 窗口大小(16位长):表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控制。
实际上是不够用的。因此 TCP 引入了窗口缩放的选项,作为窗口缩放的比例因子,这个比例因子的范围在 0 ~ 14,比例因子可以将窗口的值扩大为原来的 2 ^ n 次方。 - 校验和(16位长):对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段。
- 紧急指针(16位长):本报文段中的紧急数据的最后一个字节的序号。
- 选项字段—最多40字节。每个选项的开始是1字节的kind字段,说明选项的类型。
- 0:选项表结束(1字节)
- 1:无操作(1字节)用于选项字段之间的字边界对齐。
- 2:最大报文段长度(4字节,Maximum Segment Size,MSS)通常在创建连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
- 3:窗口扩大因子(3字节,wscale),取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
- 4:sackOK—发送端支持并同意使用SACK选项。
- 5:SACK实际工作的选项。
- 8:时间戳(10字节,TCP Timestamps Option,TSopt),组成如下:kind(1 字节) + length(1 字节) + info(8 个字节) 。
info由两部分组成: timestamp和timestamp echo,各占 4 个字节。- 发送端的时间戳(Timestamp Value field,TSval,4字节)
- 时间戳回显应答(Timestamp Echo Reply field,TSecr,4字节)
TCP 的时间戳主要解决两大问题:
- 计算往返时延 RTT(Round-Trip Time)
比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么
step 1: a 向 b 发送的时候,timestamp中存放的内容就是 a 主机发送时的内核时刻ta1。
step 2: b 向 a 回复 s2 报文的时候,timestamp中存放的是 b 主机的时刻tb,timestamp echo字段为从 s1 报文中解析出来的 ta1。
step 3: a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值 - 防止序列号的回绕问题
因为每次发包的时候都是将发包机器当时的内核时间记录在报文中,那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了。
TCP建立连接的步骤[💖]
- 最开始通信双方都处于CLOSED状态,然后服务端进程先创建传输控制块TCB并监听某个端,等待客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。【第一次握手:SYN=1,seq=x】
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。【第二次握手:SYN=1,ACK=1,seq=y,ack=x+1】
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。【第三次握手:ACK=1,seq=x+1, ack=y+1】
当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。 
服务器端资源是在完成第二次握手时分配的,而客户端资源是在完成第三次握手分配的。这就使得服务器端易于受到SYN洪范攻击。
TCP释放连接的步骤[💖]
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。【第一次挥手: FIN=1, seq=u】
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。【第二次挥手: ACK=1, seq=v, ack=u+1】
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。 - 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
【第三次挥手: FIN=1, ACK=1, seq=w, ack=u+1】 - 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。【第四次挥手: ACK=1, seq=u+1, ack=w+1】
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

参考资料: https://mp.weixin.qq.com/s/u56NcMs68sgi6uDpzJ61yw
TCP可靠传输[💖]
可靠传输就是保证接收方收到的字节流和发送方发出的字节流是完全一样的。网络层是没有可靠传输机制的,尽自己最大的努力进行交付。而传输层使用 TCP 实现可靠传输。TCP 保证可靠传输的机制有如下几种:
1. 校验和
由发送端计算待发送 TCP 报文段的校验和,然后接收端对接收到的 TCP 报文段验证其校验和(TCP 的校验和是一个端到端的校验和)。其目的是为了发现 TCP 的首部和数据在发送端到接收端之间是否发生了变动。如果接收方检测到校验和有差错,则该 TCP 报文段会被直接丢弃。<br /> UDP和TCP的校验和机制相同,都需要加上12B的伪首部。只不过UDP的校验和是可选的,而 TCP 的校验和是必须的。
2. 序号
TCP报文段首部中的序(列)号字段用来保证数据能有序的提交给应用层。TCP将数据看成一个无结构的但是有序的字节流,而序号时建立在传送的字节流上(每个字节都有一个序号)而不是建立在报文段上。序号字段的值指的是该报文段第一个字节的序号。
3. 确认应答机制
TCP首部的确认号是期望收到对方下一个报文段的第一个字节的序号。发送方缓存区会存储已经发送但未收到确认的报文段,以便在需要的时候重传。<br /> 确认应答机制就是接收方收到 TCP 报文段后就会返回一个确认应答消息。TCP使用**累计确认**:即TCP只确认数据流中至第一个丢失为止的字节。 确认应答机制和重传机制不分家,两者紧密相连。<br />
4. 重传机制
有两种事件会导致TCP对报文段进行重传:**超时**和**冗余ACK**,对应的重传机制分别为:超时重传、快速重传。
超时重传
TCP每发送一个报文段,就对这个报文段设置一次定时器,只要计时器设置的重传时间到期但还没收到确认,就重传这一段报文。对于发送方没有正确接收到接收方发来的 ACK 确认报文的情况,有以下两种情况:
- 发送方报文段丢失
- 接收方的 ACK 确认报文丢失

RTT(Round-Trip Time 往返时延):数据从网络一端传送到另一端所需的时间,也就是报文段的往返时间。
超时重传时间一般用 RTO(Retransmission Time-out)来表示,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。如果超时重传时间 RTO 远大于或小于 RTT,会发生以下情况:
- 如果RTO 远大于 RTT:重发慢,丢了老半天才重发,降低了网络传输效率,性能差。
- RTO 小于 RTT:会导致可能并没有丢就重发,于是重发的就快,加重网络拥塞,导致更多的超时,更多的超时导致更多的重发。

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。超时触发重传存在的问题是,超时周期可能相对较长。
往返时间(RTT)的计算方法
因为有重传,网络阻塞等各种变化的因素,并不能简单的计算ack时间和发送时间的差值,而是通过多次采样数值然后估算。TCP使用的方法有:
- 被平滑的RTT估计器
- 被平滑的均值偏差估计器
重传时间(RTO)的计算方法:
计算往返时间(RTT),保存测量结果,通过测量结果维护一个被平滑的RTT估计器和被平滑的均值偏差估计器,根据这两个估计器计算下一次重传时间。
超时重传会引发阻塞问题:当网络延迟突然增加时,tcp会重传数据,但是过多的重传会导致网络负担加重,从而导致更大的延时和丢包,进入恶性循环也就会导致tcp的拥塞问题。
TCP和UDP是否都需要计算往返时间RTT?
往返时间RTT只针对TCP协议,因为TCP协议需要根据RTT的值来设置超时计时器的超时时间。UDP没有确认和重传机制,因此RTT对UDP没有什么意义。
快速重传
冗余ACK:就是再次确认某个报文段的ACK,而发送方先前已经收到过该报文的确认。TCP规定每当比期望序号大的失序报文段到达时,发送一个冗余ACK,指明下一个期待字节的序号。**TCP规定每当发送方收到对同一个报文段的3个冗余ACK时,就可以认为跟在这个被确认报文段之后的报文段已丢失,这是发送方就可以对丢失报文进行重传**。通过收到冗余ACK进行重传的技术叫做快速重传。冗余ACK还被用于拥塞控制中。<br /> 举个例子:发送方已经发送 1、2、3、4、5报文段
- 接收方收到报文段 1,返回 1 的 ACK 确认报文(确认号为报文段 2 的第一个字节)
- 接收方收到报文段 3,仍然返回 1 的 ACK 确认报文(确认号为报文段 2 的第一个字节)
- 接收方收到报文段 4,仍然返回 1 的 ACK 确认报文(确认号为报文段 2 的第一个字节)
- 接收方收到报文段 5,仍然返回 1 的 ACK 确认报文(确认号为报文段 2 的第一个字节)
- 接收方收到 3 个对于报文段 1 的冗余 ACK,认为报文段 2 丢失,于是重传报文段 2
- 最后,接收方收到了报文段 2,此时因为报文段 3、4、5 都收到了,所以返回 6 的 ACK 确认报文(确认号为报文段 6 的第一个字节)

快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了超时问题,还面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。比如对于上面的例子,是重传 seq2 呢?还是重传 seq2、seq3、seq4、seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
- sack方法( Selective Acknowledgment ,选择性确认):SACK是一个TCP的选项,来允许TCP单独确认非连续的片段,用于告知真正丢失的包,只重传丢失的片段。如果要支持
SACK,必须双方都要支持。在 Linux 下,可以通过net.ipv4.tcp_sack参数打开这个功能(Linux 2.4 后默认打开)。

- D-SACK方法( Duplicate SACK ):主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。在 Linux 下可以通过
net.ipv4.tcp_dsack参数开启/关闭这个功能。D-SACK有这么几个好处:

- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是「发送方」的数据包被网络延迟了;
- 可以知道是不是网络中的路由器把「发送方」的数据包给复制了;
参考资料:
https://juejin.cn/post/6916073832335802382#heading-5
https://www.cnblogs.com/xiaolincoding/p/12732052.html
TCP流量控制[💖]
TCP提供了流量控制服务以消除发送方使接受方缓存区溢出的可能性,流量控制是一种速度匹配服务,主要是用来匹配发送方的发送效率与接收方的接受效率。**TCP的流量控制机制基于滑动窗口协议**。<br /> 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。窗口大小就是指**无需等待确认应答,可以继续发送数据的最大值**。<br /> 只有当收到了上一个报文段的确认应答后才能发送下一个报文段的这种通信模式效率非常低下(类似于停止-等待协议)。每个报文段的往返时间越长,网络的吞吐量就越低,通信的效率就越低。例如:A、B两个人对话,如果A说完一句话,就在处理其他事情,没有及时回复B,B就等着A做完其他事情后回复B,B才能说下一句话,很显然这不现实。滑动窗口协议采用**累计确认**模式,避免了前者的缺点。<br /> 假设窗口大小为 `3` 个 TCP 段,那么发送方就可以「连续发送」 `3` 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫**累计确认**。<br />
接收窗口(rwnd)
接收方自己的缓存大小即接收窗口,接受方根据接收窗口的大小动态地调整发送方地发送窗口大小。
RCV.WND:表示接收窗口的大小,它会通告给发送方。RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
RCV.NXT指针加上RCV.WND的大小,就可以指向 #4 的第一个字节了。

拥塞窗口(cwnd)
发送方根据其对当前网络拥塞程序的估计而确定的窗口值叫拥塞窗口,其大小与当前网络的带宽和时延密切相关。
发送窗口
假设下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口<br /><br /> 当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了,如下图所示。<br /><br /> 当收到之前发送的数据 `32~36` 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则**滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认**,接下来 `52~56` 字节又变成了可用窗口,那么后续也就可以发送 `52~56` 这 5 个字节的数据了。如下图所示:<br /><br /> TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
SND.WND:表示发送窗口的大小(大小是由接收方指定的);SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
SND.UNA指针加上SND.WND的大小,就可以指向 #4 的第一个字节了。

TCP报文头部有一个窗口大小字段,该字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。发送方的窗口实际大小是取rwnd和cwnd中的最小值。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
接收端会在发送 ACK 确认应答报文时,将自己的即时窗口大小(接收窗口
rwnd)填入,并跟随 ACK 报文一起发送出去。而发送方根据接收到的 ACK 报文中的窗口大小的值改变自己的发送速度。如果接收到窗口大小的值为 0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,提醒接收端把窗口大小告诉发送端。
操作系统缓冲区与滑动窗口的关系
发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。
- 当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化
考虑以下场景:- 客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为
360; - 服务端非常的繁忙,当收到客户端的数据时,应用层不能及时读取数据。
- 客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为

- 操心系统直接减少接收缓冲区大小
当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。
窗口关闭
TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是**窗口关闭**。
接收方向发送方通告窗口大小时,是通过
ACK报文来通告的。那么当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,就会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成死锁的现象。

TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
窗口探测
- 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
- 如果接收窗口不是 0,那么死锁的局面就可以被打破了。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发RST报文来中断连接。

传输层与链路层流量控制的区别
- 传输层定义了端到端用户之间的流量控制,数据链路层定义了两个中间相邻结点的流量控制;
- 数据链路层中滑动窗口协议的窗口大小是不能动态变化的,传输层中的窗口则可以动态变化。
TCP拥塞控制[💖]
**拥塞**是指:在某段时间,对网络中某一资源的需求超过了该资源所能提供的可用部分。如果网络出现拥塞,TCP 报文可能会大量丢失,此时就会大量触发重传机制,从而导致网络拥塞程度更高,严重影响传输。其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是**触发了重传机制,就会认为网络出现了拥塞**。出现拥塞时,应当控制发送方的速率,这一点和流量控制很像,但是**出发点不同**。拥塞控制与流量控制的区别在于:
- 拥塞控制是防止过多的数据注入到网络中,让网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及所有的主机、路由器以及与降低网络传输性能相关的所有因素;
- 流量控制往往是指点对点的通信量控制,即接收端控制发送端,用于抑制发送端发送数据的速率,以便接收端来得及接收。
拥塞窗口
为了调节发送方所要发送数据的量,定义了「拥塞窗口 cwnd」的概念。拥塞窗口是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化:
- 只要网络中出现了拥塞,
cwnd就会减少 - 若网络中没有出现拥塞,
cwnd就会增大
在引入拥塞窗口概念之前,发送窗口大小和接收窗口大小基本是相等的关系(取决于接收窗口大小)。引入拥塞窗口后,发送窗口的大小就等于拥塞窗口和接收窗口的最小值。
TCP的拥塞控制采用了四种算法:慢开始、拥塞避免、快重传、快恢复。
慢开始与拥塞避免
**慢开始**的思路就是:TCP 在刚建立连接完成后,如果立即把大量数据字节注入到网络,那么很有可能引起网络阻塞。好的方法是先探测一下,一点一点的提高发送数据包的数量,即由小到大逐渐增大拥塞窗口数值。**cwnd 初始值为 1,每经过一个传播轮次(一个RTT),cwnd 加倍(指数增长)**。慢开始会一直将拥塞窗口增大到门限ssthresh(阈值),然后改用拥塞避免算法。<br /> **拥塞避免**的做法是:发送端的拥塞窗口cwnd每经过一个往返时延RTT就增加1。拥塞避免的过程中,cwnd按线性规律缓慢增长(**加法增大**),而当出现一次超时(网络拥塞)时,则令慢开始的门限ssthresh等于当前cwnd的一半(**乘法减小**)。<br /> 网络拥塞时,无论在慢开始阶段还是在拥塞避免阶段,只要发送方检测到超时事件的发生(没有按时收到确认,重传计时器超时),就要把慢开始门限ssthresh设置为拥塞避免时发送窗口cwnd的一半(但不能小于2),然后把拥塞窗口cwnd重新设置为1并执行慢开始算法。这么做的目的是为了迅速减少主机发送到网络中的分组数,使发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。<br /> **拥塞避免不能完全避免拥塞,只是为了让网络不容易出现拥塞。**<br />
快重传与快恢复
快重传原理:发送方收到三个重复的 ACK 确认的时候,直接重传对方尚未收到的报文段,而不必等待哪个报文段设置的重传超时定时器。触发快速重传机制和超时重传机制的情况不同,TCP 认为触发快速重传的情况并不严重,因为大部分没丢,只丢了一小部分,快速重传做的事情有:
cwnd = cwnd/2ssthresh = cwnd- 重新进入拥塞避免阶段
快恢复原理:当发送端收到连续三个冗余ACK(重复确认)时,就执行”乘法减小”算法,把慢开始门限ssthresh设置为出现拥塞时发送方cwnd的一半。与慢开始算法将拥塞窗口设置为1不同的是,快恢复将cwnd的值设置为慢开始门限ssthresh改变后的值,然后执行拥塞避免算法(加法增大),使拥塞窗口缓慢地线程增加。由于cwnd跳过了从1开始的过程,所以叫快恢复。
有的快重传实现把开始时的拥塞窗口cwnd设置为ssthresh+3,加 3 的原因是因为收到 3 个重复的 ACK,表明有 3 个“老”的数据包离开了网络。
传输层常见问题
参考资料:
https://www.eet-china.com/mp/a44399.html
https://juejin.cn/post/6844904070889603085
1. MSS(Maximum Segment Size,最大报文长度)设置地太大或者太小会有什么影响?
网络传输过程中,数据报过大会被切分成小块。在传输层(`TCP`协议)里叫**分段**,在网络层(`IP`层)叫**分片**。无论分段还是分片都需要按照一定的长度切分,TCP里的长度称为MSS,IP里这个长度称为MTU。<br /> MSS太小时:当TCP报文段中只有1B数据时,在IP层传输数据报地开销至少有40B(TCP报文头部固定的20B+IP头部固定的20B),这样网络的利用率就不超过1/41,到了数据链路层还要加上一些开销,网络利用率更低。<br /> MSS太大时:若TCP报文太长,那么在IP层传输时,有可能要分解多个短数据报片,接收端还要将各数据报片装配成原来的TCP报文段,当传输有差错时,还要进行重传,这些都会使开销增大。<br /> 假设 MTU= 1500 byte,那么 **MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte**,如果应用层有 **2000 byte** 发送,那么需要两个切片才可以完成发送,第一个 TCP 切片 = 1460,第二个 TCP 切片 = 540。<br /> `MSS`会在三次握手的过程中传递给通信双方,用于通知对端本地最大可以接收的TCP报文数据大小(不包含TCP和IP报文首部),两者在对比后,会采用**小的**那个值作为通信的`MSS值`,这个过程叫`MSS协商`。三次握手后协商的MSS仍会改变:每次执行TCP发送消息的函数时,会重新计算一次MSS,再进行分段操作。MSS是作为可选项引入的,只不过一般情况下MSS都会传,**如果没有接收到对端TCP的MSS,本端TCP默认采用MSS=536Byte**。
2.为什么IP层会分片,TCP还要分段
MTU(Maximum Transmit Unit,最大传输单元)。 其实这个是由数据链路层提供,为了告诉上层IP层,自己的传输能力是多大。IP层就会根据它进行数据包切分。一般 MTU=1500 Byte。Linux中可以通过ifconfig查看MTU大小。
问题1:为什么IP层会分片,TCP还要分段?
假设有一份数据较大,且在TCP层不分段,如果这份数据在发送的过程中出现丢包现象,TCP会发生重传,那么重传的就是这一大份数据(虽然IP层会把数据切分为MTU长度的N多个小包,但是TCP重传的单位却是那一大份数据)。如果TCP把这份数据,分段为N个小于等于MSS长度的数据包,到了IP层后加上IP头和TCP头,还是小于MTU,那么IP层也不会再进行分包。此时在传输路上发生了丢包,那么TCP重传的时候也只是重传那一小部分的MSS段。效率会比TCP不分段时更高。
所以:数据在TCP分段,就是为了在IP层不需要分片,同时发生重传的时候只重传分段后的小份数据。
**问题2:TCP分段了,IP层就一定不会分片了吗?**<br /> 在整个传输链路中,会有多个网络层设备,而这些设备的MTU可能小于发送端的MTU。此时虽然数据包在发送端已经的传输层**分段**过了,发送端的IP层不会再分片,但如果链路上还有设备有**更小的MTU**,那么还会在这些设备上会再分片,最后所有的分片都会在**接收端**处进行组装。
问题3:IP层如何做到不分片?
如果有办法知道整个链路上,最小的MTU是多少,并且以最小MTU长度发送数据,那么不管数据传到哪个节点,都不会发生分片。整个链路上,最小的MTU,叫做PMTU(path MTU)。
Linux中可以通过PMTU Discovery方法 $cat /proc/sys/net/ipv4/ip_no_pmtu_disc 查看PMTU,该值默认为0,是开启PMTU发现的功能。一般机器上都是开启的状态。
**PMTU Discovery原理**:<br /><br /> IP数据报的报头中有一个标志位DF(Don't Fragment),当它置为1时,意味着这个IP数据报文不分片。当链路上某个路由器,收到了这个报文,当IP报文长度大于路由器的MTU时,路由器会看下这个IP报文的`DF`:如果为`0`(允许分片),就会分片并把分片后的数据传到下一个路由器;如果为`1`,就会把数据丢弃,同时返回一个ICMP包给发送端,并告诉它数据不可达,需要分片,同时带上当前机器的MTU。PMTU发现的过程:
- 应用通过TCP正常发送消息,传输层TCP分段后,到网络层加上IP头,DF置为1,消息再到更底层执行发送;
- 此时如果链路上如果有台路由器由于各种原因MTU变小了;
- IP消息到这台路由器了,路由器发现消息长度大于自己的MTU,且消息自带DF不让分片。就把消息丢弃。同时返回一个
ICMP错误给发送端,同时带上自己的MTU。 - 发送端收到这个ICMP消息,会更新自己的MTU,同时记录到一个PMTU表中。
- 因为TCP的可靠性,会尝试重传这个消息,同时以这个新MTU值计算出MSS进行分段,此时新的IP包就可以顺利被刚才的路由器转发。
- 如果路径上还有更小的MTU的路由器,那上面发生的事情还会再发生一次。
3. 为什么TCP采用三次握手建立连接,而不是不采用两次或四次握手建立连接?[💖]
- 确认通信双方的收发能力:为了实现可靠数据传输,TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知起始序列号(ISN), 并确认对方已经收到了序列号起始值的必经步骤,经过三次握手可以确定客户端和服务端的接收、发送能力都正常。
- 序列号可靠同步:如果只是两次握手, 至多只有连接发起方(客户端)的起始序列号能被确认, 服务端的起始序列号则得不到确认。如果第二次握手报文丢失,那么客户端就无法知道服务端的初始序列号,那 TCP 的可靠性就无从谈起。
- 阻止重复历史连接的初始化:客户端由于某种原因发送了两个不同序号的
SYN包,网络环境是复杂的,旧的数据包有可能先到达服务器。如果是两次握手,服务器收到旧的SYN就会立刻建立连接,那么会造成网络异常。
如果是三次握手,服务器需要回复SYN+ACK包,客户端会对比应答的序号,如果发现是旧的报文,就会给服务器发RST报文,直到正常的SYN到达服务器后才正常建立连接。 - 为什么不能是两次?
假设是两次握手建立连接,客户端发送SYN报文后该报文滞留在网络中未到达服务端,客户端超时未收到ACK报文会以为丢了包,于是重传,假设第二次传的SYN报文被服务端收到,服务端返回ACK报文,此时通信双方建立了连接。但如果客服端经过一段时间后断开了连接,此时前面滞留得SYN报文到达了服务端,服务端又会返回ACK报文建立连接,但此时客户端已经关闭,但服务端得连接还在,这就会造成服务端得连接资源浪费。 - 为什么不是三次握手?
三次握手的目的是确认双方发送和接收的能力,也可以是四次或更多次,但实际上三次就足够了。
4. 三次握手的过程中,可以携带数据吗💖
第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。<br /> 假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,疯狂的重复发 SYN 报文,这会让服务器花费大量的内存空间来缓存这些报文,这样服务器就更容易被攻击了。<br /> 对于第三次握手,此时客户端已经处于连接状态,它已经知道服务器的接收、发送能力是正常的了,所以可以携带数据是情理之中。
5. 半连接队列与SYN Flood攻击💖
三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:半连接队列和全连接队列。
半连接队列(SYN队列)
服务器第一次收到客户端的 SYN 之后回复ACK和SYN,就会处于 SYN_RCVD 状态,此时双方还没有完全建立连接。服务器会把这种状态下请求连接放在一个队列里,把这种队列称之为半连接队列(SYN队列)。
全连接队列(ACCEPT队列)
客户端返回ACK, 服务端接收后,三次握手完成,此时建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
SYN Flood攻击原理
SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击原理就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:
- 处理大量的
SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。 - 由于是不存在的 IP,服务端长时间收不到客户端的
ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。
SYN Flood 攻击得应对策略:
- 增加 SYN 连接,也就是增加半连接队列的容量。
- 减少 SYN + ACK 重试次数,避免大量的超时重发。
- 利用 SYN Cookie 技术,在服务端接收到
SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。
6. 为什么建立连接是三次握手,关闭连接是四次挥手?
其实在 TCP 握手的时候,接收端发送 `SYN+ACK` 的包是将一个 `ACK` 和一个 `SYN` 合并到一个包中,所以减少了一次包的发送,三次完成握手。<br /> 对于四次挥手,因为 TCP 是全双工通信,在主动提出关闭的一方发送 FIN 包后,接收端可能还要发送数据,不能立即关闭服务器端到客户端的数据通道,所以也就不能将服务器端的 `FIN` 包与对客户端的 `ACK` 包合并发送,只能先确认 `ACK`,然后服务器待无需发送数据时再发送 `FIN` 包,所以四次挥手时必须是四次数据包的交互。
7.TCP快速打开原理(TFO)
首轮三次握手:客户端发送SYN给服务端,服务端接收到后并不立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回。客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。
后面的三次握手:在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。
注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
TFO 的优势
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。
8. TCP四次挥手详解【💖】

不管是客户端还是服务端,都可以主动发起四次挥手,释放连接。
第一次挥手:一般情况下,主动方执行close()或 shutdown()方法,会发个FIN报文出来,表示”我不再发送数据了“。
第二次挥手:在收到主动方的FIN报文后,被动方立马回应一个ACK,意思是”我收到你的FIN了,也知道你不再发数据了”。
注意,虽然第二次和第三次挥手之间,被动方是能发数据到主动方的,但主动方能不能正常收就不一定了。
第三次挥手:在被动方在感知到第二次挥手之后,会做了一系列的收尾工作,最后也调用一个 close(), 这时候就会发出第三次挥手的FIN-ACK。
第四次挥手:主动方回一个ACK,意思是收到了。
第一次挥手和第三次挥手是在应用程序中主动触发的(比如调用close()方法),第二和第四次挥手,都是内核协议栈自动帮完成的。
8.1 FIN一定要程序执行close()或shutdown()才能发出吗?
**不一定**。一般情况下,通过对`socket`执行 `close()` 或 `shutdown()` 方法会发出`FIN`。但实际上,只要应用程序退出,不管是**主动**退出,还是**被动**退出(因为一些莫名其妙的原因被`kill`了), **都会**发出 `FIN`。
FIN 是指”我不再发送数据”,因此
shutdown()关闭读不会给对方发FIN, 关闭写才会发FIN。
8.2 如果机器上FIN-WAIT-2状态特别多,是为什么?
`FIN-WAIT-2`是**主动方**那边的状态,处于这个状态的程序,一直在等**第三次挥手**的`FIN`。而第三次挥手需要由被动方在代码里执行`close()` 发出。所以,如果机器上`FIN-WAIT-2`状态特别多,一般是因为被动方一直不执行`close()`方法发出第三次挥手。
8.3 主动方在close()之后收到的数据,会怎么处理?
**一般情况下**,程序主动执行`close()`的时候;
- 如果当前连接对应的
socket的接收缓冲区有数据,会发RST。 如果发送缓冲区有数据,那会等待发送完,再发第一次挥手的
FIN。由于TCP是全双工通信,发送数据的同时,还可以接收数据。
Close()的含义是,此时要同时关闭发送和接收消息的功能。也就是说,虽然理论上,第二次和第三次挥手之间,被动方是可以传数据给主动方的。但如果主动方的四次挥手是通过close()触发的,那主动方是不会去收这个消息的。而且还会回一个RST。直接结束掉这次连接。
8.4 第二第三次挥手之间,不能传输数据吗?
不一定。前面提到的`Close()`的含义是,要同时**关闭发送和接收**消息的功能。如果通过调用shutdown()做到**只关闭发送消息**,**不关闭接收消息**的half-close功能,那就能继续收消息了。
int shutdown(int sock, int howto);其中 howto 为断开方式。有以下取值:SHUT_RD:关闭读。这时应用层不应该再尝试接收数据,内核协议栈中就算接收缓冲区收到数据也会被丢弃。SHUT_WR:关闭写。如果发送缓冲区中还有数据没发,会将将数据传递到目标主机。SHUT_RDWR:关闭读和写。相当于close()了。
8.5 被动方如何知道主动方socket执行了close还是shutdown?
不管**主动**关闭方调用的是`close()`还是`shutdown()`,对于被动方来说,收到的就只有一个`FIN`。第二次挥手和第三次挥手之间,如果**被动**关闭方想发数据,那么在代码层面上,就是执行了 `send()` 方法。 `send()` 会把数据拷贝到本机的**发送缓冲区**。如果发送缓冲区没出问题,都能拷贝进去,所以正常情况下,`send()`**一般**都会返回成功。
int send( SOCKET s,const char* buf,int len,int flags);

然后被动方内核协议栈会把数据发给主动关闭方。
- 如果上一次主动关闭方调用的是
shutdown(socket_fd, SHUT_WR)。那此时主动关闭方不再发送消息,但能接收被动方的消息,一切如常,皆大欢喜。 - 如果上一次主动关闭方调用的是
close()。那主动方在收到被动方的数据后会直接丢弃,然后回一个RST。
针对第二种情况。被动方内核协议栈收到了RST,会把连接关闭。但内核连接关闭了,应用层也不知道(除非被通知)。
此时被动方应用层接下来的操作,无非就是读或写:
- 如果是读,则会返回
RST的报错,也就是常见的Connection reset by peer。 - 如果是写,那么程序会产生
SIGPIPE信号,应用层代码可以捕获并处理信号,如果不处理,则默认情况下进程会终止,异常退出。
总结一下,当被动关闭方 recv() 返回EOF时,说明主动方通过 close()或 shutdown(fd, SHUT_WR) 发起了第一次挥手。如果此时被动方执行两次 send()。
- 第一次
send(), 一般会成功返回。 - 第二次
send()时。如果主动方是通过shutdown(fd, SHUT_WR)发起的第一次挥手,那此时send()还是会成功。如果主动方通过close()发起的第一次挥手,那此时会产生SIGPIPE信号,进程默认会终止,异常退出。不想异常退出的话,记得捕获处理这个信号。
8.5 如果被动方一直不发起第三次挥手,会发生什么?
第三次挥手,是由被动方主动触发的,比如调用close()。如果由于代码错误或者其他一些原因,被动方就是不执行第三次挥手。
这时候,主动方会根据自身第一次挥手的时候用的是 close() 还是 shutdown(fd, SHUT_WR) ,有不同的行为表现。
- 如果是
shutdown(fd, SHUT_WR),说明主动方其实只关闭了写,但还可以读,此时会一直处于FIN-WAIT-2, 死等被动方的第三次挥手。 - 如果是
close(), 说明主动方读写都关闭了,这时候会处于FIN-WAIT-2一段时间,这个时间由net.ipv4.tcp_fin_timeout控制,一般是60s,这个值正好跟2MSL一样 。超过这段时间之后,状态不会变成**TIME-WAIT**,而是直接变成**CLOSED**。
# cat /proc/sys/net/ipv4/tcp_fin_timeout60
8.6 为什么TIME_WAIT 状态需要经过 2MSL 才能返回到 CLOSE 状态?【💖】
`MSL` 是报文在网络中最大生存时间(Maximum Segment Lifetime)。
- 原因一:在客户端发送对服务器端的
FIN的确认包ACK后,这个ACK包是有可能不可达的,服务器端如果收不到ACK的话需要重新发送FIN包。所以客户端发送ACK后需要留出2MSL时间(ACK 到达服务器 + 服务器发送 FIN 重传包,一来一回)等待确认服务器端确实收到了 ACK 包。也就是说客户端如果等待2MSL时间也没有收到服务器端的重传包FIN,说明可以确认服务器已经收到客户端发送的ACK。 - 原因二:在客户端发送完最后一个
ACK报文段后,在经过2MSL时间,就可以使本连接持续的时间内所产生的所有报文都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文。有些路由器会缓存 IP 数据包,如果连接重用了,那么这些延迟收到的包就有可能会跟新连接混在一起。 - 如果不等待2MSL,客户端直接断开,当服务端还有很多数据包要给客户端发,且还在路上的时候,若客户端的端口此时刚好被新的应用占用,那么就接收到了无用数据包,造成数据包混乱。
8.7 TCP有没有可能出现三次挥手?【💖】
有可能。TCP四次挥手里,第二次和第三次挥手之间,是有可能有数据传输的。第三次挥手的目的是为了告诉主动方,”被动方没有数据要发了”。**所以在第一次挥手之后,如果被动方没有数据要发给主动方。第二和第三次挥手是有可能合并传输的**。这样就出现了三次挥手。<br /><br /> 如果第二、第三次挥手之间**有数据**要发也可能变成三次挥手。TCP中有个特性叫**延迟确认**。可以简单理解为:**接收方收到数据以后不需要立刻马上回复ACK确认包。**在此基础上,**不是每一次发送数据包都能对应收到一个 **`**ACK**`** 确认包,因为接收方可以合并确认。**而这个合并确认,放在四次挥手里,可以把第二次挥手、第三次挥手,以及他们之间的数据传输都合并在一起发送。因此也出现了三次挥手。<br />
8.8 TCP两次挥手
正常情况下TCP连接的两端,是不同**IP+端口**的进程,关闭的时候双方都**发出了一个FIN和收到了一个ACK**。但如果TCP连接的两端,**IP+端口**是一样的情况下,那么在关闭连接的时候,也同样做到了**一端发出了一个FIN,也收到了一个 ACK**,只不过正好这两端其实是`同一个socket` 。**这种两端IP+端口都一样的连接,叫TCP自连接。**<br />**自连接实现**
# -p 可以指定源端口号。也就是指定了一个端口号为6666的客户端去连接 127.0.0.1:6666# nc -p 6666 127.0.0.1 6666# netstat -nt | grep 6666tcp 0 0 127.0.0.1:6666 127.0.0.1:6666 ESTABLISHED
**相同的socket,自己连自己的时候,握手是三次的。挥手是两次的。**<br />正常情况下的的三次握手如下<br />**自连接的解决方案**:只要能保证客户端和服务端的端口不一致就行。<br /> 写代码的时候一般不会去指定客户端的端口,系统会随机给客户端分配某个范围内的端口。而这个范围,可以通过下面的命令进行查询。只要服务器端口不在`32768-60999`这个范围内,比如设置为`8888`。就可以规避掉这个问题。
# cat /proc/sys/net/ipv4/ip_local_port_range32768 60999
另外一种方案是在连接建立完成之后判断下IP和端口是否一致,如果遇到自连接,则断开重试。
9. 四次握手
前面提到的`TCP`自连接是一个客户端自己连自己的场景。不同客户端之间也可以互联,这种情况叫**TCP同时打开**。<br /> **TCP同时打开**在握手时的状态变化,跟TCP自连接是非常的像:
比如
SYN_SENT状态下,又收到了一个SYN,其实就相当于自连接里,在发出了第一次握手后,又收到了第一次握手的请求。结果都是变成SYN_RCVD。在
SYN_RCVD状态下收到了SYN+ACK,就相当于自连接里,在发出第二次握手后,又收到第二次握手的请求,结果都是变成ESTABLISHED。他们的源码其实都是同一块逻辑。

**实现TCP同时打开**:分别在两个控制台下执行下面命令
while true; do nc -p 2224 127.0.0.1 2223 -v;done # 控制台Awhile true; do nc -p 2223 127.0.0.1 2224 -v;done # 控制台B
一开始会疯狂失败,重试. 一段时间后,连接建立完成.
# netstat -an | grep 2223Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 127.0.0.1:2224 127.0.0.1:2223 ESTABLISHEDtcp 0 0 127.0.0.1:2223 127.0.0.1:2224 ESTABLISHED
期间抓包获得下面的结果<br />**总结**:
- 四次挥手中,不管是程序主动执行
close(),还是进程被杀,都有可能发出第一次挥手FIN包。如果机器上FIN-WAIT-2状态特别多,一般是因为对端一直不执行close()方法发出第三次挥手。 Close()会同时关闭发送和接收消息的功能。shutdown()能单独关闭发送或接受消息。- 第二、第三次挥手,是有可能合在一起的。于是四次挥手就变成三次挥手了。
- 同一个socket自己连自己,会产生TCP自连接,自连接的挥手是两次挥手。
- 没有
listen,两个客户端之间也能建立连接。这种情况叫TCP同时打开,它由四次握手产生。
10.Nagle算法与延迟确认
Nagle算法
场景:如果发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块,提高网络的利用率。
Nagle算法的基本定义是 任意时刻,最多只能有一个未被确认的小段。 ”小段“指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。Nagle算法的规则:
(1)如果包长度达到MSS,则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
延迟确认
场景:当客户端/服务端收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?延迟确认就是稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。
有一些场景是不能延迟确认的,收到了就要马上回复:
- 接收到了大于一个 frame 的报文,且需要调整窗口大小;
- TCP 处于 quickack 模式(通过
tcp_in_quickack_mode设置); - 发现了乱序包。
Nagle和延迟确认同时使用的问题:前者意味着延迟发,后者意味着延迟接收,会造成更大的延迟,产生性能问题。
11.TCP粘包问题【💖】
参考资料:https://segmentfault.com/a/1190000039691657
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包;从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段,基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能
产生原因:
- TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
- 应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;
发生粘包、拆包的情况:
- 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
- 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
- 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
解决办法
由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,主要有如下解决办法:
- 消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
- 将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
UDP不会发生粘包问题
UDP(User Datagram Protocol),用户数据包协议。是面向无连接,不可靠的,基于**数据报**的传输层通信协议。基于**数据报**是指无论应用层交给 UDP 多长的报文,UDP 都照样发送,即一次发送一个报文。至于如果数据包太长,需要分片,那也是IP层的事情,大不了效率低一些。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。IP的报头里面是有一个 16 位的总长度的,意味着 IP 报头里记录了整个 IP 包的总长度。**UDP报头中有**`**16bit**`**用于指示 UDP 数据报文的长度,假设这个长度是 n ,以此作为数据边界。因此在接收端的应用层能清晰地将不同的数据报文区分开,从报头开始取 n 位,就是一个完整的数据报,从而避免粘包和拆包的问题。**即使没有UDP数据报报头的16bit长度,由于IP的头部已经包含了数据的总长度,此时如果IP包里使用的是UDP协议,那么IP包里的总长度就包含了UDP头部和UDP数据。 UDP数据的长度 = IP 总长度 - IP 头部长度(20B) - UDP 头部长度(8B)。<br /><br /><br />
IP层不会发生粘包问题
IP分包与重组过程:
- 如果消息过长,
IP层会按 MTU 长度把消息分成 N 个切片,每个切片带有自身在包里的位置(offset)和同样的IP头信息。 - 各个切片在网络中进行传输。每个数据包切片可以在不同的路由中流转,然后在最后的终点汇合后再组装。
- 在接收端收到第一个切片包时会申请一块新内存,创建IP包的数据结构,等待其他切片分包数据到位。
等消息全部到位后就把整个消息包给到上层(传输层)进行处理。
IP 层从按长度切片到把切片组装成一个数据包的过程中,都只管运输,都不需要在意消息的边界和内容,都不在意消息内容了,那就不会有粘包一说了。
12. TCP Keep-Alive与HTTP Keep-Alive
- HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;
- TCP 的 Keep-Alive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;
HTTP的Keep-Alive
HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 **HTTP 短连接**,如下图:<br /><br />HTTP的Keep-Alive使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 **HTTP 长连接**。HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。<br /><br />在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加:`Connection: Keep-Alive`,然后当服务器收到请求,作出回应的时候,它也添加一个头在响应中:`Connection: Keep-Alive`。这样连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接,这一直继续到客户端或服务器端提出断开连接。**从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive**,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加:`Connection:close`。
HTTP 长连接不仅仅减少了 TCP 连接资源的开销,而且这给 HTTP 流水线技术提供了可实现的基础。HTTP流水线是指:客户端可以先一次性发送多个请求,而在发送过程中不需先等待服务器的回应,可以减少整体的响应时间。
例如,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。HTTP 流水线机制则允许客户端同时发出 A 请求和 B 请求。如上图所示。但是服务器还是按照顺序响应,先回应 A 请求,完成后再回应 B 请求。此外,服务器响应完客户端第一批发送的请求后,客户端才能发出下一批的请求,也就说如果服务器响应的过程发生了阻塞,那么客户端就无法发出下一批的请求,此时就造成了「队头阻塞」的问题。
TCP的Keep-Alive
TCP的Keep-Alive就是TCP的保活机制。工作原理如下:<br /> 定义一个时间段,在该时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用,每隔一个时间间隔发送一个探测报文,该探测报文包含的数据量非常少,如果连续几个探测报文都没有得到响应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知上层应用程序。Linux中`SO_KEEPALIVE`用于开启或者关闭保活探测,默认情况下是关闭的。当`SO_KEEPALIVE`开启时,可以保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。可以通过下述命令查看其相关属性:
# 保活时间, 默认未7200秒, 即2小时内如果没有相关连接活动则会启动保活机制cat /proc/sys/net/ipv4/tcp_keepalive_time7200# 每次检测时间间隔, 默认为75秒cat /proc/sys/net/ipv4/tcp_keepalive_intvl75# 探测次数,默认为9次。如果9次探测报文均无响应则认为对方不可达,从而中断连接cat /proc/sys/net/ipv4/tcp_keepalive_probes9# 也可用下述命令查看上述三个参数sudo sysctl -a | grep keepalive# Linux中最少需要经过2小时11分15秒才能发现一个死亡连接, 计算方式如下tcp_keepalive_time + (tcp_keepalive_intvl * tcp_keepalive_probes)
如果 TCP 连接的两端一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,连续几次没有响应(达到保活探测次数后),TCP 会报告该 TCP 连接已经死亡。
TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活,这个工作是在内核完成的,如下图:
SO_KEEPALIVE默认的时间间隔太长,不利于应用程序检测连接状态。有两种解决方法:
全局设置:在Linux中可以通过修改 /etc/sysctl.conf 的全局配置
net.ipv4.tcp_keepalive_time=7200net.ipv4.tcp_keepalive_intvl=75net.ipv4.tcp_keepalive_probes=9
添加上面的配置后输入
sysctl -p使其生效。可以通过命令sysctl -a | grep keepalive来查看当前配置。如果应用中已经设置SO_KEEPALIVE,程序不用重启,内核直接生效。这种方法设置的全局的参数,针对整个系统生效,对单个socket的设置不够友好。针对单个连接设置:可以使用TCP的
TCP_KEEPCNT、TCP_KEEPIDLE、TCP_KEEPINTVL3个选项。这些选项是连接级别的,每个socket都可以设置这些属性。可以通过man 7 tcp查看上述属性含义。代码层面的设置步骤如下: ```c int keepAlive = 1; // 非0值,开启keepalive属性 int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行此TCP层的探测 int keepInterval = 5; // 探测发包间隔为5秒 int keepCount = 3; // 尝试探测的最多次数
// 开启探活 setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (void )&keepAlive, sizeof(keepAlive)); setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, (void)&keepIdle, sizeof(keepIdle)); setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, (void )&keepInterval, sizeof(keepInterval)); setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, (void )&keepCount, sizeof(keepCount) ```
心跳包
心跳包每隔固定时间发一次,以此来告诉服务器,这个客户端还活着,是用来**及时检测**是否断线的一种机制,属于应用程序协议的一部分。TCP中的心跳机制:SO_KEEPALIVE选项,默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。TCP中的Keep-Alive机制一般是用来检测长时间不活跃的连接的,不适合用来及时检测连接状态。<br /> 心跳包具有更大的灵活性,应用层可以控制检测的时间间隔、检测方式等。心跳包同时适用于TCP和UDP。某些情况下,心跳包还可以附带一些信息,定时在服务端和客户端之间同步。需要及时检测TCP连接状态,心跳包(HeartBeat)还是必须的。
13.假设互联网中所有链路的传输都不出差错,所有结点都不发生故障,这种情况下TCP的”可靠交付”功能是否多余?
不多余。在以下情况下,TCP的可靠交互功能必不可少:
- 每个IP数据包独立选择路由,因此在到达目的主机时可能出现失序;
- 由于路由选择的计算出现错误,导致IP数据报首部的TTL数值下降到0,这个数据报就会被丢失;
- 某个路由器突然出现大量通信,以致于路由器来不及处理到达的数据报,因此有的数据报被丢弃;
以上问题表明:必须依靠TCP的可靠交付功能才能保证目的主机的目的进程收到正确的报文。
