简介
TCP(Transmission Control Protocol,传输控制协议)是互联网传输层的核心协议之一,基于 OSI 四层模型中的传输层,为应用层提供 面向连接的、可靠的、有序的字节流传输服务。与无连接的 UDP 不同,TCP 通过复杂的控制机制确保数据在不可靠的网络环境中稳定传输,广泛应用于 HTTP、FTP、SMTP 等对可靠性要求高的场景。
TCP协议格式
源/⽬的端⼝号: 表⽰数据是从哪个进程来, 到哪个进程去;
• 32位序号/32位确认号:
序号:为每个字节提供唯一标识,解决乱序、重复问题,是数据重组和去重的依据。
确认号:通过累计确认机制高效反馈接收状态,驱动滑动窗口和重传机制,确保发送方 “知所发,知已收”。
二者与 TCP 的其他机制(如滑动窗口、拥塞控制)深度协同,在不可靠的网络环境中构建了稳健的数据传输链路。理解这两个字段的设计逻辑,是深入掌握 TCP 协议的关键一步。
• 4位TCP报头⻓度: 表⽰该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最⼤⻓度是15 *4 = 60
• 6位标志位:
◦ URG: 紧急指针是否有效
◦ ACK: 确认号是否有效
◦ PSH: 提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛
◦ RST: 对⽅要求重新建⽴连接; 我们把携带RST标识的称为复位报⽂段
◦ SYN: 请求建⽴连接; 我们把携带SYN标识的称为同步报⽂段
◦ FIN: 通知对⽅, 本端要关闭了, 我们称携带FIN标识的为结束报⽂段
• 16位窗⼝⼤⼩: 16 位窗口大小是 TCP 流量控制的基础机制,虽受限于 64KB 范围,但通过窗口扩大选项扩展到 GB 级别,适应了高带宽网络的需求
• 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含
TCP⾸部, 也包含TCP数据部分.
• 16位紧急指针: 标识哪部分数据是紧急数据;
确认应答(可靠性机制)
对每个字节数据都进行编号,即序列号
每个ACK都带有确认序号,返回给发送者,发送者便知道下一次从哪里开始发送。提高了其数据传输的可靠性。
超时重传(可靠性机制)
1.发送超时
主机A传输数据给主机B时可能因为网络阻塞等原因无法传输给主机B。如果在超过特定时间内主机B没有接收到数据,主机A进行重发。
也有可能是ACK丢失
2.接收方接收到了数据,但返回应答超时
连接管理(可靠性机制)
三次握手建立连接:
客户端 → SYN → 服务端
客户端 ← SYN+ACK ← 服务端
客户端 → ACK → 服务端
四次挥手断开连接:
主动方 → FIN → 被动方
主动方 ← ACK ← 被动方
主动方 ← FIN ← 被动方
主动方 → ACK → 被动方
通过TIME_WAIT状态(默认2MSL)确保最后一个ACK可靠到达,避免报文残留。
较粗的虚线表⽰服务端的状态变化情况;
• 较粗的实线表⽰客⼾端的状态变化情况;
• CLOSED是⼀个假想的起始点, 不是真实状态
TIME_WAIT 是 TCP 连接关闭过程中,主动发起关闭的一方(通常是客户端) 在完成四次挥手后进入的一种中间状态。它位于 FIN-WAIT-2 状态 之后,持续一段时间(通常为 2 倍的最大段生存时间,即 2MSL),最终才会进入 CLOSED 状态。
滑动窗口(效率机制)
窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值. 上图的窗⼝⼤⼩就是4000个字节(四个段).
• 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
• 收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推;
• 操作系统内核为了维护这个滑动窗⼝, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
• 窗⼝越⼤, 则⽹络的吞吐率就越⾼
那么如果出现了丢包, 如何进⾏重传? 这⾥分两种情况讨论:
情况一:数据包已到达,ACK丢失
如图:部分ACK丢失,可以根据后续ACK确认。
情况二:
当某⼀段报⽂段丢失之后, 发送端会⼀直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是1001” ⼀样;
• 如果发送端主机连续三次收到了同样⼀个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
• 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 “⾼速重发控制”(也叫 “快重传”)
流量控制(效率机制)
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继⽽引起丢包重传等等⼀系列连锁反应.
因此TCP⽀持根据接收端的处理能⼒, 来决定发送端的发送速度. 这个机制就叫做流量控制(FlowControl);
• 接收端将⾃⼰可以接收的缓冲区⼤⼩放⼊ TCP ⾸部中的 “窗⼝⼤⼩” 字段, 通过ACK端通知发送端;
• 窗⼝⼤⼩字段越⼤, 说明⽹络的吞吐量越⾼;
• 接收端⼀旦发现⾃⼰的缓冲区快满了, 就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;
• 发送端接受到这个窗⼝之后, 就会减慢⾃⼰的发送速度
如果接收端缓冲区满了, 就会将窗⼝置为0; 这时发送⽅不再发送数据, 但是需要定期发送⼀个窗⼝探测数据段, 使接收端把窗⼝⼤⼩告诉发送端
接收端如何把窗⼝⼤⼩告诉发送端呢? 回忆我们的TCP⾸部中, 有⼀个16位窗⼝字段, 就是存放了窗⼝⼤⼩信息;
那么问题来了, 16位数字最⼤表⽰65535, 那么TCP窗⼝最⼤就是65535字节么?
实际上, TCP⾸部40字节选项中还包含了⼀个窗⼝扩⼤因⼦M, 实际窗⼝⼤⼩是 窗⼝字段的值左移 M 位;
阻塞控制(可靠机制)
作用:通过网络的畅通程度来控制窗口大小
发送开始的时候, 定义拥塞窗⼝⼤⼩为1;
• 每次收到⼀个ACK应答, 拥塞窗⼝加1;
• 每次发送数据包的时候, 将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较, 取较⼩的值作为实际发送的窗⼝;像上⾯这样的拥塞窗⼝增⻓速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增⻓速度⾮常快.
• 为了不增⻓的那么快, 因此不能使拥塞窗⼝单纯的加倍.
• 此处引⼊⼀个叫做慢启动的阈值
• 当拥塞窗⼝超过这个阈值的时候, 不再按照指数⽅式增⻓, ⽽是按照线性⽅式增⻓
• 当TCP开始启动的时候, 慢启动阈值等于窗⼝最⼤值;
• 在每次超时重发的时候, 慢启动阈值会变成原来的⼀半, 同时拥塞窗⼝置回1;少量的丢包, 我们仅仅是触发超时重传; ⼤量的丢包, 我们就认为⽹络拥塞;
延迟应答(效率机制)
如果接收数据的主机⽴刻返回ACK应答, 这时候返回的窗⼝可能⽐较⼩.
• 假设接收端缓冲区为1M. ⼀次收到了500K的数据; 如果⽴刻应答, 返回的窗⼝就是500K;
• 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
• 在这种情况下, 接收端处理还远没有达到⾃⼰的极限, 即使窗⼝再放⼤⼀些, 也能处理过来;
• 如果接收端稍微等⼀会再应答, ⽐如等待200ms再应答, 那么这个时候返回的窗⼝⼤⼩就是1M;
⼀定要记得, 窗⼝越⼤, ⽹络吞吐量就越⼤, 传输效率就越⾼. 我们的⽬标是在保证⽹络不拥塞的情况下尽量提⾼传输效率;那么所有的包都可以延迟应答么? 肯定也不是;
• 数量限制: 每隔N个包就应答⼀次;
• 时间限制: 超过最⼤延迟时间就应答⼀次;
具体的数量和超时时间, 依操作系统不同也有差异; ⼀般N取2, 超时时间取200ms;
捎带应答
当给主机A发送响应数据时,如果ACK需要传输则两个报文可以合并成一个,减少了通信次数,提高其传输效率。
粘包问题
• ⾸先要明确, 粘包问题中的 “包” , 是指的应⽤层的数据包.
• 在TCP的协议头中, 没有如同UDP⼀样的 “报⽂⻓度” 这样的字段, 但是有⼀个序号这样的字段.
• 站在传输层的⻆度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.
• 站在应⽤层的⻆度, 看到的只是⼀串连续的字节数据.
• 那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应⽤层数据包.
那么如何避免粘包问题呢? 归根结底就是⼀句话, 明确两个包之间的边界.
• 对于定⻓的包, 保证每次都按固定⼤⼩读取即可; 例如上⾯的Request结构, 是固定⼤⼩的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
• 对于变⻓的包, 可以在包头的位置, 约定⼀个包总⻓度的字段, 从⽽就知道了包的结束位置;
• 对于变⻓的包, 还可以在包和包之间使⽤明确的分隔符