传输层(TCP+UDP)
LAB:Student Resources | Kurose/Ross, Computer Networking: a Top-Down Approach, 8/e
微信:MMTLS TCP Tencent/mars
QQ:TLS UDP HTTP
端口
port
0-65536 TCP Header中为16位无符号整数
IP地址能够标识主机,但是通信是由主机中的应用进程发起和接收的,必须让应用进程拥有一个标识,而本地进程ID(PID)在各个OS之间并不统一(因为要写入TCP头部,而各个OS发送的TCPHeader必须统一),如果是给特定的应用进程分配一个特定的ID,显然也不行,因为通信双方并不必须知道对方的真正身份,因此,最后的结果就是在传输层和应用层的界面上开一些特定的“门”,进程需要的时候就分配给他,这就是软件端口(port)的由来。
服务器端口号
- 1-1023: 标准规定的应用强制占用。
HTTP | HTTPS | DNS | RIP | BGP | Telnet | FTP | SMTP | SSH | RMI |
---|---|---|---|---|---|---|---|---|---|
80 | 443 | 53 | 520 | 179 | 23 | 21 | 25 | 22 | 111 |
- 1024-49151: 登记端口。
客户端端口
49152-65535(16384个): 客户端发起connect动态分配,属于临时端口号,断开连接或者通信结束就会收回。
一些系统中可能会出现超过65536的情况,但是由于TCP Header的限制,必须要对端口号 % 65536
如何提高服务器并发能力
一般来讲,通过增加服务器内存、修改最大FD个数等,可以做到单台服务器支持10万+的TCP并发。当然,在真实的商用场景下,单台服务器都会编入分布式集群,通过负载均衡算法动态的调度不同用户的请求给最空闲的服务器,如果服务器平均内存使用超过80%的警戒线,那么就会及时采用限流或者扩展集群的方式来保证服务,绝对不会出现服务器的内存被耗尽的情况,那样就算事故了。
TCP UDP绑定相同端口
IP数据报中的协议字段可以区分TCP还是UDP,因此靠这个字段就能将IP数据报准确交给对应的协议软件实现,然后软件根据抽象的端口找到应用进程,两个协议的端口并不是一个域。
多个TCP 服务进程绑定相同端口
IP不同,端口相同,也是可以的。
0.0.0.0:8888 表示监听所有IP地址的8888端口
客户端端口的复用
可以,因为TCP连接有4个元素才能唯一确定,只要有一个不一样就是不同的TCP连接
UDP
4.17 如何基于 UDP 协议实现可靠传输? | 小林coding
User Datagram Protocol 用户数据报协议
特点
- 无连接:传输数据之前不需要建立起连接,直接发送即可,可以是一对一,多对一,一对多,多对多。
- 不可靠:可能出现差错,丢失,重复,不能保证按序到达,也就是尽最大努力交付,这也使得首部开销比较小。
- 面向数据报:无论应用层交给 UDP 多长的报文,UDP 都照样发送,即一次发送一个报文。对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。而接收方在接收数据报的时候,也不会像面对 TCP 无穷无尽的二进制流那样不清楚啥时候能结束。
- 没有拥塞控制
优点还有可扩展性强:TCP实现固化在操作系统中,有时性能可能不能满足部分需求,开发者可以利用UDP的特性,在应用层实现可靠传输,比如QUIC协议
UDP Datagram
总长度8B,源端口和目的端口各占2B,然后是UDP的PDU长度(2B),最后是检验和(2B)。
- udp长度:max: 65535B min: 8B 整个UDP数据报的长度
- 检验和:添加伪首部[源IP(4B), 目的IP(4B), 全0(1B), 17[1B,表示UDP协议类型], UDP长度(2B)],计算检验和先将检验和位置0,然后将添加了伪首部的整个UDP数据报划分成若干个16位字,不够补零,最后将这些字按位相加,高位进位溢出进到低位,最后取反码放到检验和。IP packet只检验首部,而UDP datagram全部都检验。
源端口 | 目的端口 | UDP数据报长度 | 检验和 |
---|---|---|---|
2B | 2B | 2B | 2B |
如果UDP发现检验和不正确,就直接丢弃数据报
如果UDP发现目的端口不准确,就丢弃数据报,随后由ICMP发送一条“终点不可达”的差错报告报文(traceroute)
TCP
Transmission Control Protocol,传输控制协议
特点
- 面向连接:传输数据之前必须建立起双方的连接,传输完双方应该断开连接,只能是点对点通信。
- 可靠:通过TCP传送的数据,无差错,不丢失,不重复,按序到达,这也使得首部开销比较大。
- 全双工:双方可以同时收发信息,设有接收和发送缓冲区
- 面向字节流:将应用层交下来的数据不是以消息报为单位向目的主机发送,而是看作无结构的字节流,TCP不懂字节流的含义是什么,这些数据可能被切割和组装成各种数据包,但是发送者发出的字节流和接受者收到的字节流必须一样,并且应用能够正确识别这些无意义字节流的含义,将其还原为有意义的应用层数据。接收端收到这些数据包后没有正确还原原来的消息,就会有“粘包”的现象。
可靠传输协议:ARQ
ARQ(Automatic Repeat reQuest,自动重传请求)是计算机网络中用于确保数据可靠传输的一种关键技术。它主要通过确认(ACK)和超时重传两种机制,在不可靠的网络服务上实现可靠的数据传输。当发送方在一定时间内未收到确认帧时,它会自动重发数据包,直到收到确认为止。ARQ协议分为停止等待ARQ和连续ARQ两种类型,每种都有其特定的应用场景和优缺点。
- ARQ是一种可以在不可靠的数据通道上可靠地传输数据的方案,所以其实链路层和传输层都用了ARQ,并不专属某一层。
- 并不是一条连接只要有一层用了ARQ,它的上层的通信就是可靠的。因为ARQ只保证使用它的点到点是可靠的,比如数据链路层只保证你和你的路由器通信可靠,你的路由器到小区的路由器通信也可靠, 但是路由器本身会故障,会拥塞丢包,也就是点本身会产生问题。
- 所以需要在传输层或者应用层再加一层ARQ保障整条数据通道的可靠性。比如你自己写程序要在应用层通信,但传输层不用tcp想用udp,也可以在你程序里用ARQ协来实现可靠性。
- 注意: 在发送完一个分组后,必须暂时保留已发送的分组的副本,以备重发。
- 分组和确认分组都必须进行编号。
- 超时计时器的重传时间应当比数据在分组传输的平均往返时间更长一些。
停止-等待ARQ的逐步优化
v2.x
v2.0:给数据包加上校验和,防止数据包出现比特差错
- 2.0的第一个问题:接收方的ACK可能有比特差错
- 2.0的第二个问题:发送方未正确接收ACK直接重发,接收方不会区分是不是重传的包,导致交给上层重复的数据。
v2.1如何解决v2.0问题?
- 给ACK加一个校验和。
- 发送方收到ACK,说明是接收方正确收到了,发送下一个数据包。
- 发送方收到NAK或者校验和出错的包,选择重发这个数据包。
此时还没有解决第二个问题,接收方正常接收发出ACK,如果发送方收到一个校验和错误的响应(本来应该是ACK)然后重传,但是接收方会把重传的包当成全新的,这样就导致了重复的问题。
- 在不丢包的情况下,给数据包附上一bit标识符,让接收方区分是否为重发的数据包。
- 发送方发的时候就注明是0号数据包,接收方鉴别无误就可以转换到准备接收下一个数据包的状态并发送ACK,如果发送方收到了ACK,皆大欢喜继续发下一个数据包
- 如果发送方并未正确收到ACK,则需要重发此包,而接收方早就转换到准备接收新数据包的状态了,再次接收到旧数据包直接选择丢弃,但也要记得再发一下ACK,提醒发送者可以发送新数据包了。
v2.2:改善接收方的响应结构
去除NAK,在ACK内部用一个比特位表示ACK或者NAK
- 接收方准备接收0号,如果出错,则发送ACK
1,发送方仍处于等待ACK0的状态,收到的只要不是ACK0,就会重发。 - 接收方准备接收0号,如果正确,则发送ACK
0,准备接收新数据包,而发送方如果没有正确收到这个ACK0,会再次重发,这时候接收方会再次发送ACK0让发送方知晓 上个数据包已经被正确接收,提醒发送者可以开始新数据包,然后把这个重复的丢弃。
v2.x的问题
如果数据包丢了,接收方收不到自然也谈不上响应,再如果,响应丢了,响应迟到了,发送者就会陷入空等状态
v3.0:比特交换协议
可靠的传输协议,也叫比特交换协议,在在v2可以解决丢包和迟到的问题。
- 从发送方角度考虑,把超时作为重传的唯一根据。
如果在准备接收ACK
1的情况下收到错误响应(ACK0或者校验和错误),则什么也不做,等待超时重发发出分组即启动timer,一定时间内没有收到正确的响应到了timeout,则重发并重启timer;
收到正确的响应就停止timer,转换到准备发下一个分组的状态,如果有这个如果这时有响应发来,肯定是迟到的响应,不予理会。
- 因为有传播时延和排队处理时延,所以可能会出现过早超时然后重发的情况
接受者的角度肯定是收到1,ACK
1然后准备接收0,这时候再次接到1,此时回答一个ACK1,然后丢弃重复的1。发送者如果在等待上方传下来数据的情况下收到了响应,说明这肯定是一个迟到的响应,不予理会。
v3总结:
发一个数据包,等对应的ACK,超时了就重发,必须且只能收到一次对应的ACK,收到就转变状态不等了,来再多也没用。
v3.0的问题
- 时间利用上不如v2,但是基于时间的重传和基于ACK比特位的重传是冲突的,因此问题不算大。
- 最根本的还是停止等待对时间资源的浪费。
Go Back N(回退N步):流水线式的发送与接收
Go Back N:发送窗口,累计确认,超时重传。
发送方:
- 维护一个发送窗口(N),用来限制一次最多发送的包数目,基于超时重传
- 在窗口以前的默认已经成功发送了(0-base),窗口内部有的已经发送处于等待响应的状态(base-nextseqnum),有的还没有发送过(nextseqnum+1 - base+N-1)。
- 应用层传下来的data,如果分配到的序号超过窗口,拒绝(或者缓存一下),在窗口内,填到nextseqnum++处。
- 在发送窗口中的包,发送base时start timer,一直把从base到nextseqnum的包都发出去,如果timeout就把这些包全部重发一遍。
- 接收ACK
- 根据ACK中的编号滑动窗口边缘(base=getacknum+1) 这里的ACK包含的序号是接收方封装的,接收方的逻辑可以保证此序号之前的全部正确传输。
- 如果滑动窗口之后,base = nextseqnum 说明窗口内已经没有要发送的分组,stop timer,如果还有,那就restart timer,继续等待。
接收方:
- 累计确认:只需要维护一个序号expectedseqnum,顾名思义,接收方必须按序ACK,按序递交给上层。
- 收到的包确实是自己想要的,于是就发一个ACK
expectedseqnum,随后递交给应用层,然后ex…num自增,表明自己要接收下一个包- 假定ACK响应中的序号N,对于接收方来说序号<=N的全部正确递交给上层了。
- 没有收到自己想要的包,就丢弃,并把上次制作好的ACK重新发送,提醒发送方该滑动窗口了。(可能是因为发送方没有正确识别ACK造成的冗余分组)
意味着即使之前已经接受过正确的分组也要丢弃,expectedseqnum之后的情况是未知的,因此只能回退重传。
单个分组的错传,会引起之后的大量分组重传。
Selective Repeat(选择重传):无需按序ACK,不累计确认
Selective Repeat 特点:接收窗口,乱序ACK,按序交付上层
接收方不丢弃正确的乱序分组,而是先进行ACK然后缓存,并不直接交给上层。
- 对于发送方来说,对方对base序号的ACK是发送窗口滑动的唯一标准。
- 对于接收方来说,成功按序递交给上层是接收窗口滑动的唯一标准。
发送方:
- 维护发送窗口:
- 对于应用层传下来的数据,仍然不变。
- 在发送窗口的包,为每一个包都设置一个timer,单独计时。
- 接收ACK:
- ACK对应编号标记为已ACK,
- 如果ACKnum = send_base,则说明可以移动了,移动到第一个未ACK的序号处
- 例子:窗口变成OOOOXXXXOXX,则滑动后的窗口为XXXXOXXXXXX
接收方:可以不按序ACK,但是递交给上层需要按序,维护一个接收窗口
- 序号在窗口里面的,没问题的就发个ACK
- 失序的(在窗口中间的)先缓存好,等于rcv_base则准备交付给应用层,将从rcv_base开始所有缓存好的包 连续、按序交给应用层
冗余分组:
对于接收者来说,ACKnum=rcv_base就可以开始滑动了,表明rcv_base之前的数据肯定已经正确交付给应用层了,但是并不能保证这个ACK就一定能被正确解析,因此发送窗口可能会迟迟不滑动导致一直重传(冗余分组)
- 如果序号是[base-N 到 base-1]的,即使之前ACK过了,也还是会回复ACK,不断尝试提醒发送方滑动发送窗口。
窗口注意事项
- 接收窗口与发送窗口并不总是完全的同步,可能会错开一部分,不过接收窗口的base也不会完全超过发送窗口,毕竟如果还没发送,也就谈不上接收过了。
序号范围与窗口大小序号是循环利用的,如果窗口太大,序号范围太小,就有可能发生重传的分组被当成是新分组的情况
序号是0~3 窗口大小为3,0 1 2 ACK过了传给上层,接收窗口滑动变成 3 0 1
ACK没有被正确接收,因此0,1,2重传,此时0和1就被当成是全新的数据了。
窗口大小与序号的关系计算
假设序号0-N-1,窗口大小为M,M <= N 发送窗口为序号0到M-1
- 对于SR来说,最坏的情况,接收窗口为序号M到M+M-1(一共M个),而2M-1不能超过序号N-1,否则就会有上面的问题,因此$M\le \frac{N}{2}$
- 对于GBN来说,窗口只有一个宽度,因此$M\le N-1$
可靠传输协议的关键
确认机制:没差错要ACK
- 保证没有比特差错:分组和ACK都要有校验位。
对于超时(未得到正确的ACK)重传
- 发送时使用timer进行计时,超时则重传,
- 可能带来的冗余分组问题,要让接收方能分辨出冗余分组
- 接收方如何对待冗余分组:发送者给分组添加序号,接收方根据序号分辨这是一个重传的还是新的分组。
- 并且发出的ACK也要携带序号,提醒哪个分组被正确收到了
- 接收方如何对待冗余分组:发送者给分组添加序号,接收方根据序号分辨这是一个重传的还是新的分组。
窗口、流水线发送:提高信道利用率
- 窗口大小:窗口之间不同步(无法避免)但是窗口太大,导致重传分组被当做新的分组。
一个默认的假设:除了丢包,包并不会被重新排序。
而现实是传输层下方是不可靠信道,并不保证数据准确无误并且一定按序到达,因此应当将互联网看成是一个不定时发送的缓存,由于序号复用,这样可能会有相同序列号的分组出现在信道中,产生冲突。假定一个分组在网络中有TTL,超过TTL,序号就一定能够被再次使用。
GBN vs SR
特性 | GBN | SR |
---|---|---|
重传策略 | 从丢失的分组开始,重新发送整个窗口 | 仅重传出错或超时的分组 |
接收端处理 | 按顺序接收分组,否则丢弃 | 接收乱序分组并缓存 |
计时器数量 | 1 个计时器 | N 个计时器(每个分组一个计时器) |
适用场景 | 高丢包率但传输顺序严格的环境 | 低丢包率,允许乱序接收的环境 |
发送窗口 | N | N |
接收窗口 | 1 | N |
Go-Back-N (GBN)
- 特点:
- 发送端最多可以连续发送 N 个未确认的分组。
- 如果在超时时间内没有收到某个分组的确认,应从该分组开始 重新发送其后所有分组。
- 计时器需求:
GBN 协议只需要一个 **==全局计时器==**,用于跟踪 最早发送但尚未确认的分组(窗口起点)。- 一旦计时器超时,直接 回退到该分组并重传整个窗口,不需要为每个分组单独计时。
- 原因是 GBN 要求分组必须 按顺序到达,只要有一个分组超时,所有后续分组都需要重发。
Selective Repeat (SR)
- 特点:
- 支持接收端 乱序接收,允许正确接收的分组先缓存。
- 发送端只重传超时或出错的分组,而不是整个窗口。
- 计时器需求:
由于 SR 可以 选择性重传特定分组,每个分组都需要一个 **==独立的计时器==**。- 如果某个分组超时,只重传该分组,而不影响其他分组。
- 因此,发送端必须为 窗口内的每个分组维护单独的计时器,以便精确控制每个分组的超时和重传行为。
基于字节流
数据包分片
- 以太网帧总长度至少64B,数据负载不能超过MTU,首部+FCS 18 因此要求 IP数据报长度在
46~1500
Byte
IP首部至少20B,因此传输层数据包为
26~1480
ByteTCP首部20B TCP报文段的数据部分为
6~1460
ByteUDP首部8B UDP数据报的数据部分为
18~1472
Byte这些都是为了迎合以太网帧的帧大小限制。当超过了这个限制,就要对IP数据包进行分片。
UDP:无法在传输层分片
超过了数据部分的大小只能通过IP进行分片,分多个IP数据报发送。
它并没有协商的能力,所以它只能直接把用户发送的数据,传给网络层(IP层),由网络层来进行分片。
对 网络层(IP层)来说:它并不知道上层传过来的数据,到底是 TCP 还是 UDP,它并不关心也没有能力区分。
如果发现数据过大,那么 IP 层会自动对数据进行切割,分片。用 UDP 协议发送,那么如果网络发生了波动,丢失了某个 IP 包分片, 对于 UDP 而言, 它没有反馈丢失了哪个分片给发送方的能力,这就意味着:50k 的数据全都丢失了,如果需要重传,就得再次完整的传递这 50k 的数据。
UDP 协议头有 2 byte 表示长度的字段。所以实际 UDP 数据包的长度不能超过65507字节(65,535 − 8字节UDP报头 − 20字节IP头部)
TCP 是流数据,没有该限制。
而 TCP 只会重传这一个丢失的分片包。
所以如果一个应用采用 UDP 来通讯,一般都会特意控制下单个包体的大小,从而提高传输效率。
TCP:可以在传输层协商自行分片
最大分段大小(Maxitum Segment Size, MSS)
这里首先要说下:MSS(Maxitum Segment Size)最大分段大小,它是 TCP 协议里面的一个概念。
MSS 要保证一个TCP报文段,加上TCPIP首部长度以后,适合单个链路层帧。
TCP 在建立连接的时候,会协商双方的MSS值,通常这个 MSS 会控制在 MTU 以内:最大 IP 包大小减去 IP 和 TCP 协议头的大小。(其最终目的:就是尽量避免 IP 分片)1500-20-20 = 1460
这样 TCP 就可以在自己这一层,把用户发送的数据,预先分成多个大小限制在 MTU 里的 TCP 包。每个 TCP 的分片包,都完整了包含了 TCP 头信息,方便在接收方重组。
如果某些情况导致:已经分好的 TCP 分片,还是大于了 MTU,那就在 IP 层中,再执行一次分片。
这个时候如果数据丢了,那也只需要重传这一个 TCP 的分片,而不是整个原始的 50k 数据。
而 IP(RFC 791)中规定所有主机或路由器必须能够接受576字节以内的数据报,576字节以上不保证能接受,有一定可能会分片。RFC791(IP协议)——协议格式_rfc 791-CSDN博客
严格讲,这并非是协商出来一个统一的MSS值,TCP允许连接两端使用各自不同的MSS值。例如,这会发生在参与TCP连接的一台设备使用非常少的内存处理到来的TCP分组。
基于字节流的解决方案
应用层传到 TCP 协议的数据,不是以数据报为单位向目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各种数据包,接收端(应用层)收到这些数据包后没有正确还原原来的消息,因此出现粘包现象。
正因为基于数据报和基于字节流的差异,TCP 发送端发 10 次字节流数据,而这时候接收端可以分 100 次去取数据,每次取数据的长度可以根据处理能力作调整;但 UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完,且发了多少,就取多少,确保每次都是一个完整的数据报。
根本原因是应用层不知道消息的边界在哪里,不知道字节流的开始和结束位置,错误地划分了数据包序列中间的边界
- 定长:FTP
分隔符:SMTP HTTP
可以通过特殊的标志作为头尾,比如当收到了
0xfffffe
或者回车符,则认为收到了新消息的头,此时继续取数据,直到收到下一个头标志0xfffffe
或者尾部标记,才认为是一个完整消息。类似的像 HTTP 协议里当使用 chunked 编码 传输时,使用若干个 chunk 组成消息,最后由一个标明长度为 0 的 chunk 结束。TLV:HTTP Content-Length WebSocket Protobuf Thrift
这个一般配合上面的特殊标志一起使用,在收到头标志时,里面还可以带上消息长度,以此表明在这之后多少 byte 都是属于这个消息的。如果在这之后正好有符合长度的 byte,则取走,作为一个完整消息给应用层使用。在实际场景中,HTTP 中的
Content-Length
就起了类似的作用,当接收端收到的消息长度小于 Content-Length 时,说明还有些消息没收到。那接收端会一直等,直到拿够了消息或超时,关于这一点上一篇文章里有更详细的说明基于TLV的协议,接收段不断的检查Tag,如果收到tag就会去取length,这里面有一个点就是tag和length是定长的,比如tag是四个字符,length占4个字节。取到length以后就读取length个字节的value。理论上value后面接着的就是下一个tag
Netty 解决方案
定长解码器
FixedLengthFrameDecoder
分割字符解码器
DelimeterBasedFrameDecoder
长度字段解码器
LengthFieldBasedFrameDecoder
面向连接的 TCP 协议实现
TCP连接可以由一个四元组(源IP, 源PORT, 目的IP, 目的PORT)唯一确定服务器的目的IP, 目的PORT一般是不会变化的,因此理论上TCP最大支持的连接数是 $2^{32} \times 2^{16} = 2^{48}$个,而TCP在linux系统中的实现用的是一个叫socket的编程接口实现的,socket本身就是一个文件,一个TCP连接就创建一个SOCKET FD因此还应该考虑服务器最大的内存大小。
TCP Segment
PSH = 1,数据不会缓存,立即交给上层(PUSH)
URG = 1,紧急数据指针指定了
可靠传输(reliable transmission)
一个TCP报文段中的数据部分长度不能超过MSS,可以包含若干字节。
累积确认(cumulative acknowledgement)
在TCP中并无数据长度的说法,在接收端和发送端眼中数据是没有边界、没有长度、但有序的字节流,用序号来标识字节。
- TCP Segment中的序号(Seq)代表第一个字节在发送端的序号。
- 初始序号可以是随机的
- 发送者用确认号(ACK)来提醒对方,自己下一个想要接收对方的Seq = ACK的字节。
按序接收
TCP只记录第一个字节流是有序的,因此TCP也应按序接收,ACK = n,代表着序号n以前的数据都被正确接收。只确认到第一个丢失字节为止的位置。
失序:回顾可靠传输协议GBN与SR,一个重要的区别就是SR对于失序的报文会选择先缓存再发送ACK,而GBN会直接丢弃,TCP的实现就是先保留,然后等待缺少的字节填补间隔
捎带确认:发送数据方同时也可以是接收数据方,这样可以在携带数据的报文中捎带进行确认。比如 Telnet echo
功能中,服务端会把确认号装在回复给客户端的报文中,与此同时还运输了数据
无数据的ACK报文:有的报文只是有一个确认的功能,没有带任何数据,但是Seq字段也不能空,所以还是会填Seq字段,只是一个逻辑上的标号
超时重传(timeout retransmission)
RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传。
RTO 的确定是一个关键问题,因为它直接影响到 TCP 的性能和效率。如果 RTO 设置得太小,会导致不必要的重传,增加网络负担;如果 RTO 设置得太大,会导致数据传输的延迟,降低吞吐量。因此,RTO 应该根据网络的实际状况,动态地进行调整。
RTT 的值会随着网络的波动而变化,所以 TCP 不能直接使用 RTT 作为 RTO。为了动态地调整 RTO,TCP 协议采用了一些算法,如加权移动平均(EWMA)算法,Karn 算法,Jacobson 算法等,这些算法都是根据往返时延(RTT)的测量和变化来估计 RTO 的值。
超时间隔是通过统计学加上适当的估计算出来的,但是必须大于 1 RTT+路由器处理时延。
不采用重传后的样本(Karn 算法)因为不知道这个ACK是对重传的ACK还是迟到的ACK,因此不统计重传的RTT
如果突然变得拥塞,导致大量超时重传,无法统计样本的RTT,造成RTT无法及时更新,因此每次重传都把RTO翻倍,也是一种拥塞控制的机制,防止过度阻塞
快速重传(fast retransmit)
防止间隔加倍导致网络时延过大,乱序到达则发送冗余ACK,收到3次冗余ACK,就重传一次对应ACK序号的数据。
事件 | TCP 接收方操作 |
---|---|
收到按顺序的分段,其序列号是接收方期望的。所有数据的序列号都小于或等于期望的序列号并已被确认。 | 延迟发送ACK。等待最多500毫秒以接收下一个按顺序到达的分段。如果在此时间间隔内未接收到下一个分段,则发送一个ACK。 |
收到按顺序的分段,其序列号是接收方期望的,并且有另一个按顺序的分段正在等待ACK发送。 | 立即发送单个累计ACK,确认两个按顺序的分段。 |
收到乱序的分段,其序列号大于期望的序列号。检测到数据的缺口(gap)。 | 立即发送冗余ACK,表明下一期望接收的字节的序列号(即缺口的起始序列号)。 |
收到的分段能够部分或完全填补接收数据中的缺口。 | 立即发送ACK,前提是该分段的起始序列号正好是缺口的起始序列号。 |
GBN协议中,用变量expectedseqnum表示expectedseqnum以前的数据都已经正确接收, 接收方将不停发送具有expectedseqnum1的ACK(之前已经发过了,因此是冗余ACK),直到正确收到具有expectedseqnum1的数据。==GBN==
选择重传 SACK
改进的方法就是 SACK(Selective Acknowledgment),简单来讲就是在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。
**冗余SACK **
DSACK,即重复 SACK,这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
GBN or SR?
累计确认
TCP实现中包含了累计确认这个GBN的要素,但是TCP对于失序的部分不会直接丢弃,也不回复ACK,而是暂存形成一个数据缺口。
对于超时重传,TCP只会让流水线发送中第一个未确认的字节重传,并且如果接受到了序号更大的ACK,连重传也不需要;而GBN规定只要没有收到第一个的ACK,后面不管是否收到必须全部重传。
一些TCP实现中也可以采用SR,不必使用累计确认。
接收窗口
TCP有接收窗口,GBN的接收窗口宽度只有1,SR也有接收窗口,但是SR并不是累计确认。
流量控制(flow control)
不同于网络中的拥塞控制机制,流量控制是用来使发送方与接收方速率相匹配的机制,提醒发送方能发多少避免接收方缓冲区溢出。
接收窗口(rwnd)
TCP接收方维护
lastByteRcvd
(最后一个递交给应用进程的)与lastByteRead
(最后一个确认的,rcv_base或expectedseqnum)二者差值即为接收缓冲区中的现有字节数,由此可以计算出缓冲区的可用字节数字rwnd。rwnd = RcvBufferSize - (lastByteRcvd - lastByteRead)
TCP发送方维护
lastByteSent
(最后一个发送的nextSeqnum,)与lastByteAck
(最后一个确认的,send_base)二者差值即为所有已发送但未收到确认的字节数,lastByteSent - lastByteAck <= rwnd
否则就阻塞发送方,也就是说发送窗口和接收窗口是一个概念。
TCP 零窗口探测
- 如果rwnd = 0,发送方仍会发送一个特殊的1字节的段(就是下一字节的数据,没新的数据段发送的时候发一个ack),强制接收端重新宣布下一个期望的字节和窗口大小(rwnd),与此同时启动一个探测定时器(persistence timer)
- 如果接收方回复的窗口rwnd仍然为0,则发送方的探测定时器加倍。
- 没有收到ACK,在发送探测包的最大次数之后连接超时(Reset或者关闭TCP连接)
传输效率
Nagle 算法
如果数据段只有1个字符,21字节的报文段只有1B的数据,带宽利用率就很低
Nagle算法:如果连续发字节,先发一个,收到确认之后把缓存的一连串一起发出去;一旦到达发送窗口或者MSS就立即发出。
糊涂窗口综合征
- 接收窗口空出1字节就急忙通知对方,对方发过来1字节又占满缓冲区
- 接收方有足够的接收缓存再去通知,rwnd = Rcvbuffer/2 或 MSS即可通知
- 尽可能地在MSS范围内提升报文段内数据的比例,提升利用率
连接管理(connection management)
连接建立:三次握手(three-way handshake)
过程
ACK比特用于表示ACK确认号字段有效
客户端发送SYN报文:Seq = x,SYN = 1 (ACK比特 = 0)
CLOSED -> SYN_SENT
服务器SYNACK报文:Seq = y,SYN = 1,ACK确认号 = x + 1 (ACK比特 = 1)
LISTEN -> SYN_RCVD
客户端ACK报文 :Seq = x + 1,SYN = 0,ACK确认号 = y + 1 (ACK比特 = 1)
SYN_SENT -> ESTABLISHED
服务端收到ACK
SYN_RCVD -> ESTABLISHED
SYN 泛洪攻击:半连接队列溢出
客户端只 syn 不 ack。服务器收到SYN报文将会缓存y,用于核对下一个ACK的值是否为y+1,存到内存中,SYN攻击的原理就是不发送ACK,因此服务器会不断缓存y,建立许多半连接,最终队列满了,无法再握手。
解决方法就是服务器不去储存y,而是用特别的方法生成,y = H(IP1, IP2, key) 关键在于只有服务器知道的key,而合法的客户端会回复ACK报文,服务端接收以后只需要用相同的哈希函数再次计算y,看看是不是和ACK报文中的y相同,如果相同,则建立连接。
应对方式:
- SYN-cookie:如上文,开启之后即使半连接队列满了也不会丢弃 SYN 请求了。
tcp_syncookies = 1
,表示只有当 SYN半连接队列满了,才会启用。 - 增大半连接队伍长度:不能只单纯增大
tcp_max_syn_backlog
的值,还需一同增大somaxconn
和backlog
,也就是增大全连接队列。 - 减少 SYN+ACK 重传次数:
tcp_synack_retries
全连接队列溢出
调大 accpet 队列的最大长度,调大的方式是通过调大 backlog 以及 somaxconn 参数。检查系统或者代码为什么调用 accept() 不及时;
TCP 重置攻击:伪造 SYN 报文欺骗客户端主动关闭 TCP 连接
RST 用于强制中断当前的连接,如果 SYN 报文的目的端口并未有套接字在监听,说明这个请求非法,于是响应报文 RST 置 1。还有如下一种情况:

处于 ESTABLISHED
状态的服务端,如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。
接着,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,发现序列号是正确的,就会释放掉该连接。

在 TCP 重置攻击中,攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。正常情况下,如果客户端收发现到达的 Challenge ACK 对于相关连接而言是不正确的,客户端就会主动发送一个TCP 就会发送一个 RST 重置报文段,从而导致 TCP 连接的快速拆卸。
TCP 重置攻击利用这一机制,通过向通信方发送伪造的重置报文段,欺骗通信双方提前关闭 TCP 连接。如果伪造的重置报文段完全逼真,接收者就会认为它有效,并关闭 TCP 连接,防止连接被用来进一步交换信息。服务端可以创建一个新的 TCP 连接来恢复通信,但仍然可能会被攻击者重置连接。万幸的是,攻击者需要一定的时间来组装和发送伪造的报文,所以一般情况下这种攻击只对长连接有杀伤力,对于短连接而言,你还没攻击呢,人家已经完成了信息交换。
从某种意义上来说,伪造 TCP 报文段是很容易的,因为 TCP/IP 都没有任何内置的方法来验证服务端的身份。有些特殊的 IP 扩展协议(例如
IPSec
)确实可以验证身份,但并没有被广泛使用。客户端只能接收报文段,并在可能的情况下使用更高级别的协议(如TLS
)来验证服务端的身份。但这个方法对 TCP 重置包并不适用,因为 TCP 重置包是 TCP 协议本身的一部分,无法使用更高级别的协议进行验证。
连接断开:四次挥手(four-way handshake)
过程
FIN比特 置1
- 客户端发送FIN,提醒服务器要断开连接,
- 服务器随即回复ACK,表示已经收到消息准备断开,发送完剩余数据之后,在发送FIN并关闭连接,告知客户端,服务器这边已经关闭了,服务器等待最后一个ACK
- 客户端收到FIN以后开始定时并回复ACK,超时即CLOSED。服务端收到客户端ACK之后正式CLOSED
TIME-WAIT timer:如果客户端发的ack丢失,服务器还得重传Fin,如果客户端这边早早CLOSED,就收不到服务器Fin,也就发不出ack,服务器的套接字状态无法正式CLOSED
TIME_WAIT
TIME_WAIT 状态了解吗?
TIME_WAIT
是 TCP 四次挥手(四次握手断开连接)中最后一个状态,由主动关闭方(发出 FIN
的一方)进入。
它的作用主要是:防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- ✅ 确保最后一个 ACK 能被对方接收(防止对方重发最后一个 FIN,保证被动关闭方能够正确关闭);
- ✅ 等待旧连接中的残余包在网络中消失,避免和后续使用相同四元组的新连接混淆。
持续时间:一般是 2 × MSL(Maximum Segment Lifetime,最大报文生存时间),通常系统默认 MSL 为 30 秒或 60 秒,所以 TIME_WAIT 大约 1~2 分钟。
出现大量 TIME_WAIT 状态的原因有哪些?
- 服务端或客户端有大量短连接,频繁建立和关闭(比如 HTTP/1.0、早期 HTTP/1.1 没开长连接)。
- 某一端(通常是客户端)是主动关闭连接的一方,因此进入 TIME_WAIT。
- 高并发系统中,短时间内大量请求完成,连接断开,堆积了很多未清理完的 TIME_WAIT。
- 系统配置(如 Linux)未优化,TIME_WAIT 的回收速度慢。
TIME_WAIT 过多有什么危害?
- 占用过多系统资源(如内核 socket 描述符、内存等);
- 消耗可用端口号(尤其是客户端短时间大量请求同一目标地址时,端口耗尽);
- 影响新连接的建立,特别是在高并发场景下,可能出现 “bind: Address already in use” 错误。
- 降低系统吞吐量,影响整体性能。
如何减少 TIME_WAIT(宗旨:避免频繁建立和关闭 TCP 连接)
优化应用层设计、使用长连接(keep-alive)
- HTTP/1.1 开启 keep-alive,避免频繁断开重连。
- 使用更高效的协议(如 HTTP/2、HTTP/3,天然复用连接)。
- 数据库连接池(如 DBCP、HikariCP)重用连接。
避免客户端主动关闭
- 让服务端主动关闭连接,这样 TIME_WAIT 由服务端承担(服务端的心跳机制)。
调整内核参数
- 缩短 wait 时间
sysctl -w net.ipv4.tcp_fin_timeout=30
- 开启客户端 TIME_WAIT 状态下的 socket 重用
sysctl -w net.ipv4.tcp_tw_reuse=1
,开启后客户端在1s内回收,只影响 connect() 系统调用,不影响 listen()/accept()。只影响主动连接发起方(客户端侧)。- 历史 RST 报文可能会终止后面相同四元组的连接,因为 PAWS 检查到即使 RST 是过期的,也不会丢弃。
- 如果第四次挥手的 ACK 报文丢失了,有可能被动关闭连接的一方不能被正常的关闭;
tcp_tw_recycle
: 更激进,对服务端也启用快速回收,但会引发 NAT、负载均衡后的连接异常(因为它依赖时间戳回收,已在新内核废弃)。
服务器:TIME_WAIT 收到 RST
net.ipv4.tcp_rfc1337
= 0,提前释放连接。如果 = 1,丢掉 RST 报文
服务器:TIME_WAIT 收到 SYN
- SYN 报文的序列号如果大于服务端的上一个ACK报文期望的,继续进行握手。
- 序列号如果小于服务端的上个 ACK 报文期望的,RST报文。
TCP 三次挥手
当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
FIN 乱序
4.10 四次挥手中收到乱序的 FIN 包会如何处理? | 小林coding
TCP 状态转换
Client
Server
拥塞控制(congestion control)
拥塞窗口(cwnd)
- 拥塞窗口(congestion window, cwnd):取决于中间的网络条件,由发送方控制,cwnd/RTT就是发送速率。
- 接收窗口(receive window, rwnd):由接收方可用缓存控制的,限制发送速率。
- 对于发送方:
lastByteSent - lastByteAck <= min(cwnd,rwnd)
下面为了方便研究,将cwnd作为TCP发送的主要瓶颈。
基于丢包的拥塞控制(loss-based)
TCP 对于拥塞控制给出如下三个指导性原则:
如果出现丢包(冗余ACK或超时重传),那说明网络可能出现了拥塞状况,将缩短拥塞窗口,限制发送量
如果出现正常ACK,说明对方正确接收了,网络状况良好,将扩大拥塞窗口,增加发送量
带宽探测:探测拥塞开始的速率,增加发送速率以与ACK匹配,出现丢包则从该速率回退,然后继续探测。
拥塞控制算法有慢启动-拥塞避免-快速恢复三个状态,下图为FSM:
慢启动(slow-start)
初始cwnd = 1 MSS,发送1个字节,得到1 ACK, cwnd增大1 MSS,第二次发送2个字节,1个ACK增大1 MSS,呈指数增长。(2^n^ MSS per RTT)
cwnd是动态变化的,只要接收到ACK就会让cwnd增大,即使上一个RTT的ACK还没接收完,也对下一个RTT的能发送的字节数影响不大,因为接收完上一个RTT的所有ACK,意味着cwnd已经在上一RTT的基础上翻倍了,而此时肯定CWND还没被占满。
慢启动阈值ssthresh:阈值内部慢启动,超出阈值则拥塞避免
三种结束慢启动的方式:
超时重传:timeout,则将 ssthresh(slow-start threshold)设置为cwnd/2,重置 cwnd = 1 MSS,重新开始慢启动过程,然后执行重传。
可能再次发生拥塞: cwnd >= ssthresh,说明再增大可能就要再次拥塞了,应该更加谨慎地增加cwnd,进入拥塞避免模式。
快速重传:冗余ACK = 3(连续收到4个相同的ACK)触发快速重传之前,ssthresh设置为cwnd/2,cwnd减半并加上3 MSS,进入快速恢复模式。
因此,cwnd < ssthresh 仍然处于慢启动的状态。
拥塞避免(congestion avoidance)
ssthresh 是导致拥塞的cwnd / 2,因此到了第二次cwnd增大到ssthresh就应当减小增加的速度,呈线性增长(1 MSS per RTT)
如果cwnd是10个MSS大小,则在一次 RTT中发10MSS字节,假如一个报文段是1MSS,每个ACK加十分之一MSS,这些报文段全部确认之后,cwnd总共加了1MSS
两种结束拥塞避免的方式:
- 超时重传:timeout,则将ssthresh(slow-start threshold)设置为cwnd/2,重置 cwnd = 1 MSS,重新开始慢启动过程,然后执行重传。重新回到慢启动。
- 快速重传:冗余ACK = 3(连续收到4个相同的ACK)触发快速重传之前,ssthresh设置为cwnd/2,cwnd减半并加上3 MSS,进入快速恢复模式。
快速恢复(fast recovery)
快速重传以后进入快速恢复状态,既然发的是冗余ACK,说明收到的是失序的正确报文段,加3MSS更加接近实际结果。
TCP Reno vs TCP Tahoe
- Tahoe: Cut to 1 MSS when loss detected (either t-d-ACK or timeout)
- Reno: Cut to roughly half on loss detected by triple duplicate ACK
TCP Reno会在快速重传之后进入快速恢复状态,TCP Tahoe则没有快速恢复状态
AIMD:加性增乘性减
Additive-Increase, Multiplicative-Decrease 增加是一个一个加上去的,减少是立马减半的。
TCP CUBIC:更加激进但有效地探测带宽
$$
W = { \vert K - t \vert } ^3+W_{max}
$$
在Linux默认开启,在拥塞避免阶段,用一个立方函数来代替线性增长,能够在即将可能发生拥塞(到达上次开始丢包的cwnd)时放慢增长速率
其他拥塞控制方法
基于时延的拥塞控制(delay)
Loss-based: Increase sending rate until a loss (timeout) and then cut back
Delay-based: Do the same until RTT reaches RTTcongested
基于网络协作的拥塞控制(network-assisted)
TCP 公平性
if $K$ TCP sessions share same bottleneck link of bandwidth $R$, each should have average rate of $R/K$
杂项
用了 TCP 协议,数据一定不会丢吗? | 小林coding
- 数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。
- 平时没事也不用关注丢包,大部分时候TCP的重传机制保证了消息可靠性。
- 当你发现服务异常的时候,比如接口延时很高,总是失败的时候,可以用ping或者mtr命令看下是不是中间链路发生了丢包。
- TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗? | 小林coding
- HTTP/1.1 开始默认开启 Keep-Alive,同一个 TCP 连接用于多个 HTTP 的请求-响应对,属于应用层
- TCP Keepalive 属于内核,客户端和服务端长达一定时间没有交互,就会发送探测报文
HTTPS 中 TLS 和 TCP 能同时握手吗?TCP FastOpen | 小林coding
- TCP Fast Open(since Linux 3.7) 建立过一次连接之后,双方可以绕过三次握手
- TLS v1.3 的 0-RTT 恢复会话
- 总共需要 1-RTT
拔掉网线后, 原本的 TCP 连接还存在吗? | 小林coding
TCP 连接,一端断电和进程崩溃有什么区别? | 小林coding
- 拔网线、断电宕机:
- 传输数据:重传达到最大值之前插回去没事,达到最大值服务端主动关闭连接,再插上网线尝试连接就会被回复一个 RST 报文。
- 无数据传输:看开启 TCP keepalive 机制与否。如果没开启就会一直保持 ESTABLISHED
- 只进程崩溃:内核发送FIN,四次挥手关闭连接
为什么 TCP 每次建立连接时,初始化序列号都要不一样呢? | 小林coding
历史报文:新开一个TCP连接以后,收到来自上个连接的报文。
序列号随机:如果每次建立连接客户端和服务端的初始化序列号都「不一样」,大概率因为历史报文的序列号「不在」对方接收窗口,从而很大程度上避免了历史报文。
使用 tcp timestamp:能精确计算 RTT,另外一个重要的方面就是防止序列号回绕(PAWS)【增加时间戳字段长度解决回绕】
- 半连接队列满了:SYN 泛洪攻击
- LISTEN 状态:Recv_Q 表示accept队列大小,Send_Q 表示