【Linux】TCP原理
📝前言:
这篇文章我们来讲讲TCP原理
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,TCP协议格式
- 二,确认应答机制
- 1. 序号和确认序号
- 2. 捎带应答
- 三,超时重传机制
- 四,连接管理机制
- 1. 三次握手
- 2. 四次挥手
- 五,TIME_WAIT状态
- 1. TIME_WAIT的作用
- 2. 解决TIME_WAIT状态引起的bind失败
- 六,CLOSE_WAIT状态
- 七,滑动窗口
- 八,拥塞控制
- 九,延迟应答机制
- 十,面相字节流(粘包问题)
- 十一,TCP异常
- 十一,报头的封包和分用
- 十二,TCP和UDP的对比
- 1. 核心特性对比
- 2. 典型应用场景
- 3. 用 UDP 实现可靠传输
一,TCP协议格式
- 除去选项,前面的内容占20字节
- 序号和确认序号后面重点讲解
- 4位首部长度:用来标识TCP报头的长度(以四字节为单位)
- URG … FIN:标记位
URG
:紧急数据指示,让接收方优先处理ACK
:确认接收,同时携带 “确认序号”PSH
:即时通讯,让接收方 TCP 层收到数据后不缓存,直接推给应用层(避免消息延迟)(非重点)RST
:主动终止 “异常 / 无效” 的连接- 如:客户端向 “不存在的服务器端口” 发数据,服务器回复 RST=1,告知 “连接无效”,然后断开连接
SYN
:请求建立连接,用于 TCP 连接的 “建立阶段”(如三次握手的前两次),此时报文不携带应用层数据FIN
:请求释放连接,用于 TCP 连接的 “关闭阶段”,表示 “没有更多数据要发送”
- 16位窗口大小:用来进行“流量控制”,以保证效率。
窗口大小==对方缓冲区剩余空间大小
- 16位检验和:检测报文是否被损坏或篡改
- 16 位紧急指针:配合
URG
,定位 “紧急数据” 的边界- 它是 “相对于当前序号的偏移量”,用于标记 “紧急数据的末尾位置”(即 “从当前序号开始,往后偏移 N 字节是紧急数据的结束”)
二,确认应答机制
1. 序号和确认序号
- 序号:发送方对自己发送的字节流数据进行编号
- 解决乱序问题:先发的数据可能后到,但是接收方可以根据序号重新排列组织数据
- 支持超时重传机制(后面会重点讲解):发送方通过序号跟踪哪些数据已被确认,哪些需要重传
- 确认序号:用于告知发送方 “期望接收的下一个字节的序号”(ACK 被设置成
1
)- 累积确认机制(确认应答机制核心):如果应答方给发送方发送序号
n+1
,则代表前n
条数据已经全部接收到了
- 累积确认机制(确认应答机制核心):如果应答方给发送方发送序号
序号是用来描述自身报文的,确认序号是对对方报文的回应
自己发出去的报文,只有收到对方的ACK应答以后,才能确保报文有效。(即:对于最新的报文,我们其实是无法确保有效性的)
2. 捎带应答
捎带应答机制就是指:接收方在应答的时候也可以发送自己的数据。
即:
- 上图中,如果server 接受到了
client
的全部数据,应答报文中ACK
置为1
,且确认序号为301
,此时自己还想发送100
个字节的数据给client
,则此时server的应答报文里的序号为1
(如果上次收到的确认序号为1
) client
接收到server
的这100
个字节的数据以后,应答时同样可以带自己的数据。这时候应答的报文里,序号从301
开始(代表自己要发送的数据的序号开始),确认序号为101
(代表client
收到了server
的前100个字节)
三,超时重传机制
“报文丢包”场景:
- 报文真的丢了(真实丢包)
- ACK丢了(反馈异常)
- 因此,为了防止反馈异常所造成的“误判”,每个报文发出去以后,都会有一个“计时器”,当计时器到时间了,则认为真丢包了,触发重传。
- 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍(简单认为)
- 如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传,还不行就 4500ms,以此类推(指数退避策略,但是也有上限)
- 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接
理解序号的去重作用:
如果是ACK丢了,则server
就会收到client
发来的重复数据,此时server
就可以根据序号来去重。
四,连接管理机制
1. 三次握手
client和sever通信前,要先建立TCP连接,整个过程涉及三次握手(本质其实是四次,中间进行了合并)
步骤:
- client给server发送建立连接的请求:SYN
- server给client应答,同时,给client发送建立连接的请求:ACK + SYN
- client给server应答:ACK
细节:
- 原本需 4 次交互(Client 发 SYN→Server 回 ACK;Server 发 SYN→Client 回 ACK)
- 在前两步骤,是不带应用层数据的。因为:TCP 连接在第三次握手完成后才被视为 “正式建立”—— 此时双方才确认彼此的 “序号、窗口大小” 等核心参数已同步,应用层数据的传输才有可靠基础(也就是说:前两步在做准备工作)
- 三次握手的关键:用最短次数验证双向通信的可达性与参数同步。
2. 四次挥手
四次挥手是释放连接的过程,TCP 是全双工通信(双方可同时发送和接收数据),所以四次挥手是双方都关闭发送和接收的过程。
下面以 client 主动给 server 发送断开连接的请求为例:
- client 给 server 发送
FIN
(代表已经无数据发送,请求释放连接),进入FIN_WAIT_1
状态 - server 给 client 发送
ACK
,server 进入CLOSE_WAIT
状态。client 接收到ACK后,进入FIN_WAIT_2
状态 - 再由 server 给 client 发送
FIN
,sever 进入LAST_ACK
状态 - client 接收到后,再给 server 发
ACK
,进入TIME_WAIT
状态(重点)。server接收到ACK以后就立即进入CLOSED
(释放连接),但是client要在TIME_WAIT状态并等待2MSL,才能断开连接
五,TIME_WAIT状态
1. TIME_WAIT的作用
(1)确保被动关闭方(服务器)能收到最后的 ACK
TCP 是 “可靠传输协议”,其可靠性依赖于 “确认机制”—— 任何发送的报文都需要对方确认才能确保未丢失。
在四次挥手的第四步中,主动关闭方(客户端)发送给被动关闭方(服务器)的最后一个 ACK 报文,是没有后续确认的(因为服务器收到 ACK 后会直接关闭,不会再回复)。如果这个 ACK 报文因网络延迟、丢包等原因未被服务器收到,服务器会在LAST-ACK状态下超时重发FIN报文。
此时,客户端若已直接关闭(无TIME-WAIT),就无法再接收服务器重发的FIN,更无法重新发送 ACK—— 服务器会因始终收不到 ACK 而一直卡在LAST-ACK状态,导致连接资源泄漏(服务器会持续占用该连接的端口、内存等资源,无法释放)。
而TIME-WAIT的等待时间(2MSL),恰好覆盖了 “报文最大可能的往返时间”:
- 1 个 MSL:确保服务器重发的FIN报文能在网络中传输到客户端;
- 第 2 个 MSL:确保客户端重新发送的ACK报文能传输到服务器。
通过这 2 个 MSL 的等待,可 100% 确保服务器能收到最后的 ACK,正常进入CLOSED状态。
(2)避免 “旧连接的残留报文” 干扰新连接
TCP 连接的唯一标识是 “源 IP + 源端口 + 目的 IP + 目的端口”(即 “四元组”)。在实际网络中,可能存在一些因网络延迟而 “迟到” 的报文 —— 比如旧连接(已断开)中未被及时传输的数据包,会在网络中滞留一段时间(最长不超过 1 个 MSL)。
如果客户端没有TIME-WAIT,而是断开后立即用相同的四元组建立新连接,这些 “旧连接的残留报文” 可能会误闯入新连接中,被新连接的接收方(服务器)当成有效数据处理,导致数据错乱或业务逻辑异常(例如,旧报文的指令被新连接执行)。
TIME-WAIT的 2MSL 等待,能确保:
- 网络中所有属于 “旧连接” 的残留报文,都会在 1 个 MSL 内自然过期并被丢弃;
- 即使有极个别残留报文未丢弃,客户端在 2MSL 内也不会复用相同四元组建立新连接,从时间上彻底隔离旧连接和新连接,避免干扰。
2. 解决TIME_WAIT状态引起的bind失败
TIME-WAIT状态虽然保障了 TCP 连接关闭的可靠性,但在高并发场景(如电商大促、高频短连接服务)中,大量连接处于TIME-WAIT会导致端口资源被占用(无法立即复用相同四元组,即bind
原来的的端口),进而引发 “端口耗尽” 或 “连接建立延迟” 等问题,影响服务吞吐量。
解决方案:
使用setsockopt()
设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述
六,CLOSE_WAIT状态
当服务器(第二次挥手)收到客户端的 FIN 后,仅确认 “客户端已停止发送”,但服务器自身可能仍有未发送完的数据(如服务器需向客户端返回最后的响应结果),因此会先进入CLOSE-WAIT状态,待自身数据发送完毕后,再主动发送 FIN 报文,切换到LAST-ACK状态
CLOSE_WAIT 的本质是被动关闭方(服务器)未主动发送FIN报文,导致无法进入后续的LAST_ACK状态。这种情况下,fd,端口等资源被占用,导致资源泄漏。
为了避免这种情况的发生,要确保close()
被正确调用
七,滑动窗口
一发一收的效率低,那么我们可以一次发送多条数据,(其实是将多个段的等待时间重叠在一起了)
滑动窗口本质上是发送方的 “可连续发送数据的范围”,由发送缓冲区中的双指针(窗口的左、右边界)维护。在确保可靠性的前提下(通过确认应答机制),允许发送方连续发送多个数据段,无需每发一个就停下来等确认,从而将多个数据段的等待时间重叠,大幅提高传输效率。
滑动窗口的特点:
- 只能向右滑动,左边代表已经传输的数据(可丢弃),右边代表未传输的数据
- 滑动窗口的大小实时变化(取决于对方接受速度)
- 滑动窗口与确认应答配合保证传输数据的可靠性(下面以左边数据丢失为例)
- 情况一:ACK丢了
- 1001-2000的ACK丢了,但是接受到的4001-5000的ACK中确认序号是5001,则代表1001-2000已经收到。(因为确认序号的规定,只有1001-2000收到了,才可能出现2001往后的确认序号)
- 情况二:1001-2000的数据真丢了
- 此时,后面三个数据的ACK的确认序号都只会是1001(因为没收到2001之前的所有报文),这时会对1001-2000的报文进行重发
- 当server收到了1001-2000以后,再次返回时就直接是5001了(因为 2001 -5000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中)【快重传】
- 对于中间数据丢失和右端数据丢失:左端都会滑到相应位置 == 左端数据丢失
- 情况一:ACK丢了
(发送窗口)滑动窗口大小 == min(对方接受缓冲区剩余大小, 拥塞窗口大小)
八,拥塞控制
当硬件没问题的时候,网络非常拥堵,这时候就要控制client的数据包的发送速度,避免网络更加拥堵(TCP对此也有相应的拥塞控制)
这里介绍TCP拥塞控制的慢启动方法:
慢启动是 TCP 拥塞控制的起始阶段,用于在网络连接建立初期或拥塞恢复时,让发送方逐步增加发送数据量
流程:
- 初始:连接刚建立,发送方“拥塞窗口(
cwnd
)”先设为1个MSS(每次能发的最大数据段),“慢启动门限(ssthresh
)”设为较大值(比如16个MSS)。 - 增长:每收到一个确认(ACK),
cwnd
就翻倍(指数增长 - 切换:当
cwnd
涨到等于或超过ssthresh
,就从“慢启动”切到“拥塞避免”(cwnd
改成慢慢线性增长)。 - 拥塞后:若网络拥塞(比如超时没收到ACK),
ssthresh
改成当前cwnd
的一半,cwnd
重置为1个MSS,重新慢启动。
作用:
- 刚连网时,从少量少量数据开始发,试探网络能承受多少,避免一下发太多导致拥堵。
- 网络通畅顺时,快速提高发送量(指数增长),尽快达到高效传输。
- 网络堵了就重置,重新慢慢试探,防止越堵越严重。
九,延迟应答机制
TCP 接收方收到数据后,不立即发送 ACK(确认),而是等待自己有数据 / 数据更多的时候再合并并发送ACK,从而减少网络中 ACK 报文的数量,降低开销。
通常我们采用(数量限制 or 时间限制):
- 数量限制: 每隔 N 个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
例如:
- 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口
就是500K; - 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费
掉了; - 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也
能处理过来; - 所以,让接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的
窗口大小就是 1M;
十,面相字节流(粘包问题)
创建一个 TCPsocket的同时,会在内核中创建一个 发送缓冲区 和一个 接收缓冲区。TCP 报文经解包后,仅正文数据会存入接收缓冲区,且这些数据是无边界的字节流(于是就有了粘包问题)。
通常我们通过自定义用户层协议(如分隔符,长度…),确保读取的TCP报文的单个完整性
十一,TCP异常
- 进程终止: 进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别.
- 机器重启: 和进程终止的情况相同。(因为关机前会关闭所有进程)
- 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset(断开连接)。即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在。 如果对方不在, 也会把连接释放。
- 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态。 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接.
十一,报头的封包和分用
(图片来源:Linux内核中sk_buff结构详解)
简单来说:
-
sk_buff
是 Linux 网络协议栈里的“数据包容器”,用来统一管理网络包的各种信息(比如协议头、数据)。 -
当数据往 TCP 传时,
data
指针会“上移”,腾出空间给 TCP 头部。就像给礼物包快递盒,先留好贴快递单(TCP 头)的位置,再放礼物(应用数据)。 -
解包分用呢,就是收到包后,
sk_buff
带着数据,从下往上(比如从网卡到 IP 层再到 TCP 层),每一层“剥掉”自己的头,最后 TCP 层拿到应用数据,交给对应的程序。
十二,TCP和UDP的对比
1. 核心特性对比
对比维度 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
连接类型 | 面向连接(需“三次握手”建立连接,“四次挥手”关闭连接) | 无连接(直接发送数据,无需建立/关闭连接) |
数据形态 | 面向字节流(数据无固定边界,需应用层定义) | 面向数据报(每个包是独立单元,有完整边界) |
可靠性 | 可靠传输(丢包重传、顺序保证、流量/拥塞控制) | 不可靠传输(丢包不重传、无顺序、无流量控制) |
效率与开销 | 开销高(需维护连接、处理确认/重传,延迟较高) | 开销低(无连接维护,头部仅8字节,延迟低、速度快) |
数据边界 | 无天然边界(易出现“粘包”,需应用层处理) | 有天然边界(接收方按“包”接收,不粘包) |
适用数据量 | 适合大量、连续数据(如文件、流数据) | 适合少量、突发数据(如指令、短消息) |
2. 典型应用场景
TCP的核心优势是可靠传输,适合对数据完整性、顺序性要求高,且可接受少量延迟的场景:
- 文件传输:如FTP(文件传输协议)、HTTP/HTTPS(网页资源、文件下载)—— 需确保文件不丢包、不损坏。
- 数据交互:如数据库连接(MySQL、PostgreSQL)、即时通讯的“消息送达确认”(如微信文字消息)—— 需保证指令/消息准确传递。
- 流式传输:如视频会议的“关键数据”(如画面帧确认)、直播的“弹幕”—— 关键信息不能丢失。
UDP的核心优势是低延迟、低开销,适合对实时性要求高,且可接受少量丢包的场景:
- 实时音视频:如直播(抖音、快手)、视频会议(Zoom)、 VoIP(网络电话)—— 延迟比丢包更重要(少量丢包仅导致瞬间卡顿,延迟会影响交互)。
- 短指令传输:如DNS查询(域名解析,一次请求/响应仅几十字节,无需连接)、游戏指令(如王者荣耀的“移动”“攻击”指令,丢包可通过后续指令补偿)。
- 广播/组播:如局域网设备发现(如打印机搜索)、物联网传感器数据上报(大量设备高频发短数据,无需可靠确认)。
3. 用 UDP 实现可靠传输
参考 TCP 的可靠性机制, 在应用层实现类似的逻辑:
- 引入序列号, 保证数据顺序;
- 引入确认应答, 确保对端收到了数据;
- 引入超时重传, 如果隔一段时间没有应答, 就重发数据;
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!