【Linux】传输层协议TCP

报文的理解

1. TCP协议

1.1 TCP协议段格式

  • 源/目的端口号:表示数据是从哪个进程来,到哪个进程去
  • 32位序号/32位确认号
  • 4位TCP报头长:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是60
  • 6位标志位:URG:
  1. URG(紧急):紧急指针是否有效,优先传输紧急数据,不用等缓冲区满
  2. ACK(确认) :确认号是否有效,绝大多数传输报文都带 ACK
  3. PSH(推送) :接收端立即从 TCP 缓冲区读取数据,不缓存等待
  4. RST(复位) :强制断开、重新建立连接,携带 RST 的叫复位报文段
  5. SYN(同步) :请求建立连接,三次握手用,带 SYN 的叫同步报文段
  6. FIN(结束) :本端请求关闭连接,四次挥手用,带 FIN 的叫结束报文段
  • 16位窗口大小
  • 16位校验和
  • 40字节头部选项

1.2 确认应答(ACK)机制

TCP将每个字节的数据都进行了编号,即序列号每一个ACK都带有对应的确认序列号,意思是告诉发送者:收到了哪些数据,下次从哪开始发

1.3 超时重传机制

  • 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B
  • 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发
  • 因此主机B会收到很多重复数据。TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃 掉,利用序列号,就可以很容易做到去重效果

TCP 超时时间怎么确定?

  • 超时时间以 500ms 为一个时间单位
  • 第一次超时:等待 500ms 重传
  • 还没收到应答:等待 1000ms(2×500ms) 再重传
  • 还没应答:等待 2000ms(4×500ms)
  • 后续按 指数 2ⁿ × 500ms 成倍拉长等待时间
  • 重传次数累积到上限后:TCP 判定网络故障 / 对端主机异常,直接强制断开连接

1.4 连接管理机制

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接

其中三次握手本质其实是四次握手,TCP把「第二次握手的应答」和「服务端同步请求」合并捎带了,就变成三次握手

为什么要进行三次握手?

  • 以最小成本,100%确定双方通信意愿
  • 以最短方式,验证双方是支持全双工 的!

服务端状态转化:

  • CLOSED -\> LISTEN\] 服务器端调用listen后进入LISTEN状态,等待客户端连接

  • SYN_RCVD -\> ESTABLISHED\] 服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了

  • CLOSE_WAIT -\> LAST_ACK\] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)

客户端状态转化:

  • CLOSED -\> SYN_SENT\] 客户端调用connect,发送同步报文段

  • ESTABLISHED -\> FIN_WAIT_1\] 客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1

  • FIN_WAIT_2 -\> TIME_WAIT\] 客户端收到服务器发来的ACK结束报文段,进入TIME_WAIT,并发出 LAST_ACK

1.5 理解TIME_WAIT状态

当启动server绑定一个端口后,使用ctrl+c使server终止,马上再运行server会出现以下现象:

因为:虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同一个的server端口

  • TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL的时间后才能回到CLOSED状态
  • 使用Ctrl-C终止了server,所以server是主动关闭连接的一方,进入TIME_WAIT

2MSL 的两个核心作用:

  • 历史残留数据,会在 2MSL 时间内**全部自然过期消亡。**否则会出现:客户端立刻关闭、马上重启连同一个端口,旧的历史残留报文可能误入新连接,导致数据错乱、脏数据混入
  • 如果这个最后 ACK 丢包了,服务端收不到 ACK会停留在 LAST_ACK 状态,超时后,会重发 FIN 报文。客户端收到后重新补发一次 ACK

解决TIME_WAIT状态引起的bind失败的方法:

  • 使用 setsockopt ()设置 socket 描述符的 选项 SO_REUSEADDR 为 1 ,表示允许创建端口号相同 但IP地址不同的多个 socket 描述符
cpp 复制代码
int opt =1;
setsockopt(
    listenfd,        // 要设置的 socket 文件描述符(服务端监听socket)
    SOL_SOCKET,      // 选项级别:socket 层
    SO_REUSEADDR,    // 选项名:允许地址复用
    &opt,            // 传入值:1=开启
    sizeof(opt)      // 值的长度
);

1.6 滑动窗口

一发一收的方式性能较低,那么一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.
  • 发送前四个段的时候,不需要等待任何ACK,直接发送
  • 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推
  • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只 有确认应答过的数据,才能从缓冲区删掉
  • 窗口越大,证明网络越好

丢包了怎么重传?

分两种情况:

情况一 :数据包已经抵达,ACK被丢了

部分ACK丢了并不要紧,可以通过后续的ACK进行确认

情况二: 数据包直接丢了

  • 当某1001~2000报文段丢失之后,滑动窗口左侧不向右移动,发送端会一直收到1001这样的ACK
  • 如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001-2000重新发送
  • 这个时候接收端收到了1001之后,再次返回的ACK就是7001了,因为2001-7000接收端其实之前 就已经收到了,被放到了接收端操作系统内核的接收缓冲区中

这种机制被称为**"快重传"**

还有一个机制是**"超时重传"** :发送方每发送一个报文段,同时都启动超时计时器**(RTO), RTO 超时仍未收到 ACK**→ 认为丢包,立即重传该报文,并重置计时器

  • 网络轻度丢包 → 快重传快速恢复。
  • 网络严重故障、连重复 ACK 都没有 → 超时重传兜底

快重传和超时重传底层都是滑动窗口支持的

1.7 流量控制

接收端接收缓冲区容量有限,如果发送端发得太快,缓冲区被塞满,后续数据就会丢包,进而触发超时 / 快重传,降低传输效率

TCP根据接收端处理能力,限制发送端发送速率,防止接收缓冲区溢出,这个机制叫做流量控制

  • 窗口大小越大 → 接收端空闲缓冲区越多 → 发送端可以发更快,网络吞吐量越高。
  • 接收端缓冲区快满 → 把窗口调小,通过 ACK 通知发送端 → 发送端降低发送速度。
  • 接收端缓冲区完全满 → 窗口置为 0 → 发送端停止发送业务数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端

接收端如何把窗口大小告诉发送端呢?16位窗口字段

实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位

1.8 拥塞控制

虽然有了滑动窗口,但如果一次发送大量数据,仍然有可能引发问题

因为不清楚当前网络的情况,如果当前已经拥堵,则会直接加重网络拥塞,甚至丢包、超时重传,越传越堵

TCP 新增拥塞窗口cwnd ,连接初期先发少量报文段探路(慢启动),慢慢探测网络承载能力

1.9 延迟应答

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小

  • 例如:接收缓冲区总大小:1M,一次性收到:500K 数据
  • 马上回 ACK:此时缓冲区只剩 524K 空闲,通告窗口 = 524K,但应用层处理很快,10ms 就把 500K 读完了,缓冲区立刻又变回 1M 空闲
  • 缺点:立刻 ACK 报了一个偏小的窗口,浪费了接收端处理能力,发送方不敢多发,吞吐量上不去

可以稍微延迟一会

并不是所有 TCP 报文段都能无限延迟应答,有两大强制触发 ACK 的规则:

  1. 数量限制:每隔 N 个数据包,必须立刻应答一次,常规系统默认:N = 2,即收到第 2 个报文段时,必须回复 ACK,不再延迟
  2. 时间限制:就算数据包数量没凑够 N 个,超过最大延迟超时时间,也必须立即发 ACK。通用默认超时:200ms

1.10 捎带应答

客户端 / 服务端要发自己的数据 时,顺便把对对方的 ACK 确认 塞在同一个报文段里一起发,不用单独发一条纯 ACK 包,这就是捎带应答

1.11 面向字节流

创建一个 TCP Socket 时,内核会为这个连接分配两个独立的缓冲区:

  • 发送缓冲区:存放应用层要发送的数据
  • 接收缓冲区:存放从网络中收到、还未被应用读取的数据

这两个缓冲区,是 TCP 实现全双工、流量控制、可靠性传输的核心载体

  • 调用write时,数据会先写⼊发送缓冲区中
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发出
  • 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机 发送出去
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
  • 然后应用程序可以调用read从接收缓冲区拿数据
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做全双工

由于缓冲区的存在,TCP程序的读和写不需要一一匹配

1.12 粘包

TCP 协议头无 UDP 那样的报文长度字段,仅有序号字段。站在应用层的角度,仅读取到连续无分界的字节流,无法划分完整应用数据包边界,产生粘包

UDP 首部自带报文长度字段,数据有天然边界。并且一次性向应用层交付完整报文,不存在UDP粘包问题

如何解决粘包问题?手动定义数据包边界

  1. 定长包:每次按固定字节大小读取数据
  2. 变长包 - 包头标识:包头携带数据包总长度,接收端先读长度,再读取完整包体
  3. 变长包 - 分隔符:包与包之间约定特殊分隔符,保证分隔符不与正文内容冲突

1.13 TCP异常情况

  1. 进程终止:进程退出会自动释放文件描述符,内核依旧会发出 FIN 报文,四次挥手正常进行,和正常关闭连接流程无区别
  2. 机器重启:重启前系统会正常收尾,同样会触发FIN 关闭连接,效果和进程终止一致
  3. 机器掉电 / 网线断开:对方无任何报文发出,本地接收端内核仍认为连接存活 。若接收端执行写数据操作 ,立刻感知连接失效,直接收到 RST 复位 断开连接。若无读写操作,TCP 内置保活定时器,定期探测对方状态;多次无响应后,自动释放连接

应用层也可自定义心跳检测:如 HTTP 长连接定时探活、QQ 断线自动重连等机制

2. TCP总结

TCP既要保证可靠性,同时又尽可能的提高性能

可靠性:

  • 校验和
  • 序列号 (按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:

  • 定时器 (超时重传定时器,保活定时器,TIME_WAIT 定时器等)

3. TCP/UDP对比

虽然TCP 是可靠连接,但TCP 并不就优于 UDP,二者的优缺点不能简单、绝对地评判高低

TCP 侧重可靠性、有序性、不丢包、无重复,牺牲了部分传输速度和实时性

UDP 侧重传输速度快、开销小、实时性强,无连接、无握手挥手、无流量拥塞控制,允许少量丢包

基本上要可靠选 TCP,要实时、高速、广播选 UDP

4. 思考

如何用UDP实现可靠传输?

需要自定义协议,引入序列号、确认应答、超时重传、滑动窗口进行流量控制....

相关推荐
Rust研习社1 小时前
Ubuntu 全面拥抱 Rust 后,我意识到 Rust 社区要变了
linux·服务器·开发语言·后端·ubuntu·rust
csdn小瓯1 小时前
AI质量评估体系:LLM-as-a-Judge实现与自动化测试实战
前端·网络·人工智能
淼淼爱喝水2 小时前
Pikachu 靶场 RCE 模块乱码问题解决方法
网络·安全·pikachu
紫墨丹青2 小时前
贝锐向日葵IP和域名
网络·tcp/ip·网络安全·远程工作
xcLeigh2 小时前
KES大小写混合路径+国产OS/文件系统兼容实战
linux·数据库·文件系统·兼容性·麒麟·欧拉·kes
迈威通信2 小时前
戈壁滩上的“国产化通信网”:850MW光储项目如何稳定运行?
网络·物联网·安全·信息与通信
weixin_417257063 小时前
ubuntu系统-dify-相关文件配置
linux·运维·ubuntu
xiaoye-duck3 小时前
《Linux系统编程》Linux权限(下):从 umask 到粘滞位的深度解析
linux
浓黑的daidai3 小时前
day-02
linux·运维·elk