TCP协议
TCP(传输控制协议)是互联网体系中最核心、应用最广泛的传输层协议,其设计核心是提供面向连接、可靠、有序的字节流传输服务。
TCP的典型上层应用:
- HTTP:超文本传输(网页访问)
- HTTPS:加密的 HTTP 传输(安全网页)
- FTP:文件传输协议
- SSH:安全远程登录
- SMTP:邮件发送协议
- MySQL:关系型数据库通信
TCP协议的核心特点
TCP(传输控制协议)的核心特点围绕 "可靠、有序、面向连接" 三大核心目标展开。
- 面向连接:TCP 通信前必须通过 三次握手 建立双向连接,确保通信双方的能够正常接收;通信结束后必须通过 四次挥手 释放连接,避免资源泄漏。
- 可靠传输:这是 TCP 最牛的地方,通过多重机制保证数据 不丢失、不重复、不损坏。确认机制,超时重传,快速重传,校验和。
- 有序传输:**TCP 为每个字节分配唯一的 序号,**接收方会根据序列号对乱序到达的数据段进行缓存、排序,待缺失段补齐后,再按顺序交付给应用层。
- 面向字节流:TCP 不预设数据边界,发送方可以连续写入任意长度的字节数据,接收方可以按需读取任意长度的字节(字节流特性会导致粘包问题)。
- 流量控制:通过滑动窗口, 接收方在 ACK 报文中携带窗口大小反应接收缓冲区的剩余空间,发送方根据这个窗口大小调整发送速率,避免发送过快或者过慢。
- 拥塞控制:通过 慢启动、拥塞避免、快速重传、快速恢复 四大算法动态调整滑动窗口,避免网络拥塞。
TCP协议的字段解释

源/目的端口号
- 表⽰数据是从哪个进程来, 到哪个进程去;
序号(序列号)
- 表示传输的数据的第一个字节的编号,只要连接不断,序列号就会一直递增下去。 例如:若序列号为 1000,报文段携带 100 字节数据,则下一个报文段的序列号为 1100。
- 需要注意:这样并不意味着序列号是从零开始,这样设计攻击者可以轻易猜测出后续报文的序列号,进而伪造报文、篡改或劫持连接,在讲解后面建立连接时会详细说明。
确认序号
- 期望收到的下一个报文段的第一个字节的编号,值为已收到数据的最后一个字节编号 + 1 。并且只有当 ACK 标志位为 1 时,确认号才有效。(后面会详细讲解确认应答机制)。例如:收到序列号为 1000,报文段携带 100 字节数据,传输的字节编号范围是 1000 ~ 1099,确认序号则为1099+1 。
4位首部长度
- 就跟他的名字一样,表达的是TCP报头的长度(单位:字节),因为单位是字节故取值范围:最小 5(5×4=20 字节,无没有选项字段时),最大 15(15×4=60 字节,含最大长度的选项字段)。
保留位
- **它的核心作用是 为 TCP 协议的未来扩展预留空间。后续需要给 TCP 协议新增控制功能(比如新的标志位、新的传输机制),可以复用这部分保留位,而不需要重新定义整个 TCP 首部结构,从而保证协议的向后兼容性。**所以现阶段是忽略该六位的,通常至0.
6 个控制位
- URG(紧急指针有效):为 1 时,紧急指针字段有效,表示报文段中有紧急数据,优先传输。
- ACK(确认号有效):为 1 时,确认号字段才生效。
- PSH(推送数据):为 1 时,接收方应立即将数据交给应用层,而不是缓存起来。
- RST(重置连接):为 1 时,表示连接出现异常,需要强制断开并重新建立连接。
- SYN(同步序列号):为 1 时,表示请求建立连接,携带本方的初始序列号。
- FIN(释放连接):为 1 时,表示发送方没有数据要传输了,请求关闭连接。
后面在讲TCP的机制时,会讲到这六位控制位的作用。
16位窗口大小
- 表示的是接收缓冲区的剩余空间大小,用于实现流量控制,发送方的发送窗口不能超过接收方给出的窗口大小。
- 注意:这并不意味着接收缓冲区的最大空间就是2^16 - 1 = 65535字节,它的大小是由操作系统配置的,超过65535字节时就会在选项中启动扩大因子选项了,该知识了解即可。
16位校验和
- 伪首部 + TCP首部(校验和填 0) + 数据载荷 → 对数据源计算 → 得到校验和。****(接收方通过重新计算校验和并比对,就能判断收到的TCP首部和数据载荷是否完整。)
16位紧急指针
- 只有 URG 标志位为 1 时有效,指向紧急数据的最后一个字节的位置,标识紧急数据的范围。
- 需要注意的是紧急数据的长度超过65535字节,16 位的紧急指针就会出现 数值溢出。解决方法了解即可
确认应答(ACK)机制
对于TCP,为了保证数据传输的可靠性,设立了一种确认应答机制,即:一方发送报文数据给另一方,如果收到,另一方必定会给其返回确认收到的报文,即确认应答,告诉对方数据收到。

每⼀个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下⼀次你从哪⾥开始发.
超时重传机制
当主机A向主机B发送消息的时候,如果主机A在重置计时器 时间内没有收到主机B的应答报文,那么主机A就会对数据进行超时重传。同时重置计时器 (重传后的计时器通常会翻倍,避免短时间内重复重传加剧网络拥塞)
主机A没有收到主机B的应答报文的情况有两种:1、主机A发送的报文丢了。2、应答报文丢了。

连接管理机制

三次握手:建立 TCP 连接
| 方向 | 报文内容 | 状态变化 | |
|---|---|---|---|
| 第一次握手 | 客户端 → 服务器 | SYN=1,携带随机初始序列号 Seq=x |
客户端从 CLOSED 变为 SYN_SENT状态 |
| 第二次握手 | 服务器 → 客户端 | SYN=1,ACK=1,携带随机初始序列号 Seq=y 和确认号 Ack=x+1(捎带应答机制确认ACK的同时发起连接SYN) |
从 LISTEN(服务端提前调用 listen() 进入监听状态)变为 SYN_RCVD状态 |
| 第三次握手 | 客户端 → 服务器 | ACK=1,携带确认号 Ack=y+1(此时就可以携带数据了) |
双方进入 ESTABLISHED 状态 |
四次挥手:释放 TCP 连接
| 方向 | 报文内容 | 状态变化 | |
|---|---|---|---|
| 第一次挥手 | 客户端 → 服务器 | FIN=1,携带序列号 Seq=x |
主动方发送确认信号进入 FIN_WAIT_1 状态 |
| 第二次挥手 | 服务器 → 客户端 | ACK=1,确认号 Ack=x+1 |
被动方收到FIN信号进入 CLOSE_WAIT 状态 |
| 第三次挥手 | 服务器 → 客户端 | FIN=1,携带序列号 Seq=y(可能携带数据) |
被动方发起FIN信号后进入 LAST_ACK 状态 |
| 第四次挥手 | 客户端 → 服务器 | ACK=1,确认号 Ack=y+1 |
主动方收到FIN信号进入 TIME_WAIT 状态 |
为什么不像三次握手一样直接将ACK和FIN信号一起发送给客户端呢?
- 服务端可能还有数据没发完,无法立即把 ACK 和 FIN 一起发 。但是如果服务端收到客户端的 FIN 时,正好没有待发送的数据,理论上可以把 ACK 和 FIN 合并成一个包发送(此时挥手就变成 "三次")。但这种情况是少数,TCP 协议为了兼容 "服务端有数据要发" 的通用场景,默认设计为四次挥手。
总结:三次握手时双方无数据待发,所以能合并包;四次挥手时服务端可能有数据待发,必须等数据发完才能发 FIN,因此需要分开发 ACK 和 FIN。
流量控制
TCP 流量控制 的核心目的是 让发送方的速率匹配接收方的处理能力。接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继⽽引起丢包重传等等⼀系列连锁反应.
- 接收端将⾃⼰可以接收的缓冲区剩余空间⼤⼩放⼊ TCP ⾸部中的 "窗⼝⼤⼩" 字段, 通过ACK端通知发送端;
- 窗⼝⼤⼩字段越⼤, 说明⽹络的吞吐量越⾼;
- 接收端⼀旦发现⾃⼰的缓冲区快满了, 就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;
- 发送端接受到这个窗⼝之后, 就会减慢⾃⼰的发送速度;
- 如果接收端缓冲区满了, 就会将窗⼝置为0; 这时发送⽅不再发送数据, 但是需要定期发送⼀个窗⼝探测数据段, 使接收端把窗⼝⼤⼩告诉发送端.
接收端通过16位窗⼝字段, 就是存放了窗⼝⼤⼩信息;
回到之前讲的扩大因子,TCP⾸部40字节选项中还包含了⼀个窗⼝扩⼤因⼦M, 实际窗⼝⼤⼩是 窗⼝字段的值左移 M 位;
滑动窗口
确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个⽐较⼤的缺点, 就是性能较差. 尤其是数据往返的时间较⻓的时候.
既然这样⼀发⼀收的⽅式性能较低, 那么我们⼀次发送多条数据, 就可以⼤⼤的提⾼性能(其实是将多个段的等待时间重叠在⼀起了).


- 窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值**(具体是由十六位窗口大小,和发送方根据网络拥塞状态动态调整的(拥塞控制))**. 上图的窗⼝⼤⼩就是4000个字节(四个段).
- 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
- 收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗⼝, 需要开辟 发送缓冲区 来记录当前还有哪些被发送过去的数据没有应答; 只有确认应答过的数据, 才能从窗口中缓冲区删掉;
- 窗⼝越⼤, 则⽹络的吞吐率就越⾼;
滑动窗口是不怕应答丢失的,因为就算某一个应答丢失还可以通过后续的ACK来进行确认。
滑动窗口原生机制(依赖超时重传)下,接收方接收正确的数据段做出回复之后,倘若此时第二段数据丢失,之后在收到乱序数据时虽然会接收该数据但它不会做应答,不再发送任何 ACK。
而发送方在发送
1001-2000时,会启动一个 超时计时器(RTO) 。当计时器到期,发送方依然没收到任何 ACK,就判定1001-2000段丢失,从滑动窗口的 "已发送未确认" 区域定位这段数据,重新发送。
快重传

- 发送方发了
1001-2000、2001-3000、3001-4000、4001-5000等等七段数据; 1001-2000丢了,但2001-5000正常到了接收方;- 接收方只想要
1001-2000,所以不管收到后面哪段,都回复ACK=1001; - 发送方连续收到 3 次
ACK=1001,就立刻判定:1001-2000肯定丢了
不等待超时 :直接从滑动窗口的已发送未确认区域,定位到 1001-2000 这段数据,马上重传;
对比超时重传:不用等几十甚至几百毫秒的超时时间,毫秒级触发重传,大幅提升传输效率;
拥塞控制
TCP 拥塞控制的核心目的是 让发送方的速率匹配网络的承载能力,避免发送的数据量超过网络带宽,导致数据包延迟、丢失、网络拥堵。
两个主机在进行TCP通信的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发。但如果双方在通信时出现了大量丢包,则有可能是网络出了问题,即双方通信信道网络出现了拥塞问题。
并且网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞控制相关的算法。
所以,TCP引入了慢启动机制,在刚开始通信时先发少量的数据,弄清楚当前的网络拥堵状态,再决定按照多大的速度传输数据。

此处引⼊⼀个概念称为拥塞窗⼝,拥塞窗口(cwnd)是存储在发送方主机的操作系统内核中的一个内存变量。
- 发送开始的时候, 定义拥塞窗⼝⼤⼩为1;
- 每次收到⼀个ACK应答, 拥塞窗⼝*2;
- 每次发送数据包的时候, 将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较, 取较⼩的值作为实际发送的窗⼝;
像上⾯这样的拥塞窗⼝增⻓速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增⻓速度⾮常快.
当出现网络拥塞时,设置 ssthresh 为当前 cwnd 的一半,同时把 cwnd 重新设置成初始值,然后从新执行慢开始算法。

- 少量的丢包, 我们仅仅是触发超时重传; ⼤量的丢包, 我们就认为⽹络拥塞;
- 当TCP通信开始后, ⽹络吞吐量会逐渐上升; 随着⽹络发⽣拥堵, 吞吐量会⽴刻下降;
- 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对⽅, 但是⼜要避免给⽹络造成太⼤压⼒的折中⽅案.
- TCP拥塞控制这样的过程, 就好像 热恋的感觉
之前我们说滑动窗口大小是由16位窗口大小控制的,现在又说是由拥塞窗⼝控制的,这不是悖论吗?
- 滑动窗口大小 = min(16位窗口大小, 拥塞窗⼝大小)。
延迟应答
如果接收数据的主机⽴刻返回ACK应答, 这时候返回的窗⼝可能⽐较⼩.
- 假设接收端缓冲区为1M. ⼀次收到了500K的数据; 如果⽴刻应答, 返回的窗⼝就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到⾃⼰的极限, 即使窗⼝再放⼤⼀些, 也能处理过来;
- 如果接收端稍微等⼀会再应答, ⽐如等待200ms再应答, 那么这个时候返回的窗⼝⼤⼩就是1M;
为什么延迟应答不会降低效率?
- 延迟时间是可控的,远小于超时时间, TCP 的延迟应答有严格的时间上限(一般是 200ms,可配置),这个时间远小于超时重传的时间。
- 延迟期间接收方在 "消费数据",缓冲区在释放短期 200ms 的等待,换来的是长期更高的吞吐量,整体效率是赚的。
特殊情况:数据没被取走,延迟应答会降低效率吗?
- 会的!但 TCP 有兜底机制,尽量不让这种情况发生
- 延迟应答不是无限等,除了时间上限(200ms),还有数据量上限:如果接收方收到的数据量达到一定阈值(比如最大报文段长度(1460`)的一半),不管时间到没到,都会立即返回 ACK。
- 持续计时器的零窗口探测 只有当窗口变为 0 时,发送方会启动持续计时器,定期发 "零窗口探测报文",询问接收方窗口是否更新。接收方收到探测报文后,必须立即回复当前的窗口大小,不会再延迟 ,这就保证了发送方不会因为接收方的延迟应答而 "失联"。
⾯向字节流
调用write 时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP 的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read 从接收缓冲区拿数据;
由于缓冲区的存在, TCP程序的读和写不需要⼀⼀匹配, 例如:
- 写100个字节数据时, 可以调⽤⼀次write写100个字节, 也可以调⽤100次write, 每次写⼀个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以⼀次read⼀个字节, 重复100次;
对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个一个的字节数据,它的任务就是将这些数据可靠高效地发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
粘包 / 拆包问题
正因为 TCP 无消息边界,在实际开发中会出现 粘包 和 拆包 现象,这是面向字节流的直接产物
TCP 粘包 / 拆包的根源是面向字节流、无消息边界,必须在应用层解决:
具体怎么解决就看我们程序员自己了。这里就不举例了。
TCP 总结
可靠性保障:稳字当头
- 校验和:检测数据在传输过程中是否被篡改。
- 序列号与确认应答:给每个字节标记唯一序号,发送方通过确认号得知接收状态,实现按序交付和累计确认。
- 超时重发:发送方启动计时器,超时未收到 ACK 则重传数据,是最基础的丢包补救手段。
- 连接管理:三次握手建立连接(同步序列号),四次挥手释放连接,确保通信双方状态一致。
- 流量控制:通过十六位窗口匹配收发双方速率,避免接收方缓冲区溢出。
- 拥塞控制:通过拥塞窗口适配网络承载力,避免加重网络拥堵,保证传输稳定性。
性能优化:效率优先
- 滑动窗口:允许发送方连续发送多段未确认数据。
- 快速重传:通过 3 个重复 ACK 快速判定丢包,跳过超时等待直接重传,大幅减少丢包后的恢复时间。
- 延迟应答:接收方延迟片刻再发 ACK,换取更大的接收窗口,提升发送方的单次传输量。
- 捎带应答:将确认信息 "捎带" 在反向传输的数据报文中,减少单独发 ACK 的额外开销。