传输层协议——TCP
文章目录
- 前言
- 一、报文的理解
- 1.1 内核中的数据结构sk_buff
- 二、TCP协议段格式
- 2.1 序号
- 2.2 窗口
- 2.3 标志位
- 三、TCP协议中具体机制
- 3.1 确认应答(ACK)机制
- 丢包
- 附:超时时间的确定
- 3.2 连接管理机制(理解为什么需要连接的重要部分)
- 理解确认连接
- 3.3 滑动窗口
- 3.4 拥塞控制
- 3.5 延迟应答
- 3.6 异常问题
前言
TCP 全称为 "传输控制协议(Transmission Control Protocol"). ,这种协议是对数据的传输进行一个详细的控制;
一、报文的理解
如果应用层正在进行报文的解析,会不会影响OS从网络中读取报文呢?答案是当然可以
从操作系统对数据的处理来看,它是根据中断机制来进行COU数据的处理的,当处理报文时,会有时钟中断响应来进行进程的调度,而读取报文又会响应对应硬件中断机制,这就意味着操作系统内核上会有大量的报文需要管理
对于管理,永远绕不开:先描述,再组织!
1.1 内核中的数据结构sk_buff
sk_buff 的设计目标是高效管理网络报文的整个生命周期:从网卡接收报文(入口),到经过协议栈各层解析 / 处理(如 TCP 解包、IP 路由),再到最终发送到网卡(出口),或交付给应用程序。
sk_buff 的核心结构成员(简化版)sk_buff 结构复杂(定义在include/linux/skbuff.h),以下是相关的核心字段:
struct sk_buff {// 1. 数据缓冲区指针(核心!通过指针偏移管理各层头部)unsigned char *head; // 缓冲区起始地址(分配时固定)unsigned char *data; // 当前层数据的起始地址(随协议层变化)unsigned char *tail; // 当前层数据的结尾地址(数据长度 = tail - data)unsigned char *end; // 缓冲区结束地址(缓冲区大小 = end - head)// 2. 链表结构(用于报文队列管理)struct sk_buff *next; // 下一个skbstruct sk_buff *prev; // 上一个skb// 3. 协议相关信息__be16 protocol; // 上层协议类型(如 ETH_P_IP 表示IP协议)struct net_device *dev;// 关联的网络设备(如接收/发送的网卡)// 4. 各层协议头部指针(快速访问,避免重复解析)struct ethhdr *mac_header; // 链路层头部(如以太网头)struct iphdr *ip_header; // 网络层头部(如IP头)struct tcphdr *tcp_header; // 传输层头部(如TCP头)// 5. 生命周期管理atomic_t users; // 引用计数(0时释放缓冲区)
};
(仅辅助于理解)sk_buff 最精妙的设计是通过 head/data/tail/end 四个指针,在不移动数据的情况下,高效管理各层协议头部和数据。
- 缓冲区范围:head 到 end 是内核为该报文分配的内存缓冲区(固定大小),包含所有可能的协议头部(链路层、网络层、传输层)和应用数据。
- 当前层数据范围:data 到 tail 是 “当前协议层” 需要处理的数据(随协议层变化)。
- 以报文接收时的指针变化(从链路层到传输层)
当网卡收到一个 TCP 报文(以太网帧),sk_buff 的指针变化如下:
- 链路层(以太网):
驱动将以太网帧(含以太网头 + IP 头 + TCP 头 + 应用数据)拷贝到 sk_buff 的缓冲区,此时:
head = 缓冲区起始(指向以太网头开始)
data = head(当前处理以太网头)
tail = head + 帧总长度(指向数据结尾)
解析以太网头后,内核通过 skb_pull(skb, ETH_HLEN) 调整 data 指针(data += 以太网头长度),跳过以太网头,准备交给 IP 层。- 网络层(IP):
此时 data 指向 IP 头开始,tail 仍指向数据结尾。
解析 IP 头后,通过 skb_pull(skb, IP_HLEN) 调整 data 指针(data += IP头长度),跳过 IP 头,准备交给 TCP 层。- 传输层(TCP):
此时 data 指向 TCP 头开始,tail 指向数据结尾。
解析 TCP 头后,通过 skb_pull(skb, TCP_HLEN) 调整 data 指针(data += TCP头长度),剩下的 data 到 tail 就是应用数据(最终交付给应用程序)。
举例:报文发送时的指针变化(从传输层到链路层)
所以网络通信其实就是两个进程通过sk_buff数据结构进行通信!!
二、TCP协议段格式
//linux内核TCP数据结构
struct tcphdr {__be16 source; // 源端口号(16位,网络字节序)__be16 dest; // 目的端口号(16位,网络字节序)__be32 seq; // 序号(32位,当前报文段第一个字节的序号)__be32 ack_seq; // 确认号(32位,期望接收的下一字节序号,仅当 ACK 标志置位有效)__u8 doff; // 数据偏移(4位,单位为32位字,即 TCP 头部长度 = doff * 4 字节)__u8 res1 : 4; // 保留位(4位,未使用,必须为0)__u8 cwr : 1; // 拥塞窗口减少(CWR,1位,用于拥塞控制)__u8 ece : 1; // ECN 回声(ECE,1位,与 ECN 机制相关)__u8 urg : 1; // 紧急指针有效(URG,1位)__u8 ack : 1; // 确认号有效(ACK,1位,建立连接后通常置位)__u8 psh : 1; // 推送数据(PSH,1位,接收方应立即将数据提交给应用层)__u8 rst : 1; // 重置连接(RST,1位,强制关闭连接)__u8 syn : 1; // 同步序号(SYN,1位,用于建立连接)__u8 fin : 1; // 结束连接(FIN,1位,用于终止连接)__be16 window; // 窗口大小(16位,接收方的接收窗口,用于流量控制)__be16 check; // 校验和(16位,用于校验 TCP 头部和数据的完整性)__be16 urg_ptr; // 紧急指针(16位,仅当 URG 置位有效,指示紧急数据在报文中的偏移)
};
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 32 位序号/32 位确认号: 详见 2.1
- 16 位窗口大小: 详见 2.2
- 4 位 TCP 报头长度: 每一位的权值表示该 TCP 头部有多少个 32 位 bit(有多少个 4 字节); 所以TCP 头部最大长度是 15 * 4 = 60,并且TCP最小为20。每位报头长度为20+4*n
- 16 位校验和: 发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此
处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分. )仅作了解(- 16 位紧急指针: 标识哪部分数据是紧急数据;
- 40 字节头部选项: 暂时忽略;
- 6 位标志位:
○ URG: 紧急指针是否有效
○ ACK: 确认号是否有效
○ PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
○ RST: 对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
○ SYN: 请求建立连接; 我们把携带 SYN 标识的称为同步报文段
○ FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段
报头和有效载荷的分离:读取前20个字节(协议标准,必须要有),再提取报头长度,将首部长度×4后减去20,再提取这个长度,至此得到并移走处理报头加选项,剩下的就是数据部分了
相比于UDP协议这里是没有报文数据长度大小的,因此操作系统是不知道TCP的整个报文长度的,它将报头和有效载荷(也就是报文数据)分离后,就不管这个数据了,将它放到自己的缓冲区后并在合适时机提交到上层应用层,让应用层来处理这个数据,这就是TCP的特性称为面向字节流的原因!!
2.1 序号
A是怎么确定B收到了消息呢?当A问B吃饭了没,B给A回答说吃过了,那么A就确定B吃过了。这一来一回就保证了从A到B的可靠性。因此对于TCP可靠性性质来说具有应答就是可靠的(即历史消息具有百分百可靠性),但对于最新的报文来说是没有应答的就意味着可靠性没法保证。这种机制被称为确认应答(双方不对应答做应答)
对于TCP来说,一次发送多个报文无疑是更有性价比的,也可以说TCP 倾向于批量发送,以减少头部对带宽的浪费。
- 若一次只发送一个小报文(如仅 1 字节数据),头部可能占比 95% 以上(20 字节头部 + 1 字节数据),有效数据传输率极低。
- 若一次发送多个报文(或合并多个小数据为较大的报文),头部总开销占比会显著降低。例如,10 个报文的总头部为 200 字节,若每个携带 1000 字节数据,总数据量 10000 字节,头部占比仅 2%,有效利用率大幅提升。
对于TCP来说,当发送多个报文并确认应答的时候,此时TCP头部的序号和确认序号就可以发挥作用了。(确认序号=序号+1)
在看以下图片的时候,请带着这样子的理解:每一个箭头其实都是TCP报文,至少也是一个TCP报头!而所谓的数字就是报头中的序号位段
应答的话只要有一个序号结构就行了啊,为什么在TCP报头中会存在序号和确认序号两个序号结构呢?
通信是双方的,A给B发消息,B给A应答的时候,可以捎带着自己的数据给A,这种应答叫做捎带应答。这样结合在一起,可以批量化发送报文,通信的效率就更高了。
2.2 窗口
当对端的TCP接受缓冲区满了该怎么办呢?继续发?然后让对方丢弃自己发的报文?这样做当然不行,面临效率低下且占用资源的问题,此时TCP报头中的16位滑动窗口就可以发挥作用了。
只需要让TCP按需按量发送,即知道对端的接受缓冲区大小,把自己缓冲区剩余大小添加到16位窗口大小,这种机制称为流量控制(作用是合理发送数据,主要站在效率角度,考虑了可靠性)
2.3 标志位
标志位本质上就是报头上比特位,为什么要有标志位呢?
TCP发送一次报文只是为了传数据嘛?传数据前也得建立连接或者断开连接,有多个客户端给服务器发送消息,根据业务类型的不同和确认是那种方式,于是就衍伸出标志位的存在来表明本次报文是干嘛的!
这里主要通过介绍三次握手引出标志位:TCP 是双向通信协议,双方(客户端和服务器)都需要确认 “自己能发送数据,且对方能接收数据”。三次握手通过三步交互,完整验证双向可达性:
- 第一次握手(客户端 → 服务器):
客户端发送 SYN 报文(syn=1),携带客户端初始序号 seq=x,表示 “我(客户端)想建立连接,我的初始序号是 x,你能收到吗?”。
→ 服务器收到后,确认 “客户端能发送,我(服务器)能接收”。 - 第二次握手(服务器 → 客户端):
服务器回复 SYN+ACK 报文(syn=1, ack=1),携带服务器初始序号 seq=y 和对客户端的确认号 ack=x+1,表示 “我收到了你的请求(x 已收到,下次请发 x+1),我的初始序号是 y,你能收到吗?”。
→ 客户端收到后,确认 “服务器能发送,我(客户端)能接收”,且 “服务器已理解我的初始序号”。 - 第三次握手(客户端 → 服务器):
客户端回复 ACK 报文(ack=1),确认号 ack=y+1,表示 “我收到了你的初始序号 y(下次请发 y+1),连接可以建立了”。
→ 服务器收到后,确认 “客户端已理解我的初始序号,且能正确回复”。
通过这三步,双方最终确认:客户端→服务器和服务器→客户端的双向通信链路都能正常工作。
在三次握手中有个隐形条件:时间!这也是为什么画图要画成斜杠式的原因。并且必须明确每一条斜杠线其实就是携带对应标志位的一个报文!!
在客户端方面它执行完发收发这三步就确定自己建立连接了,而服务器端则是收发收,那么最后一步的ACK双方是不知道是否成功的,这就导致了连接建立是否成功认知不一致问题
因此当完成三次握手但并未建立连接,客户端给服务器发带数据的报文的时候,服务器并不清楚客户端这是要干嘛,于是服务器就会发送带RST标志位的报文给客户端以求重新建立连接。通信过程中出现任何问题都可以通过在报文中设置RST标志位来重置连接。
三、TCP协议中具体机制
在上面的协议格式中都或多或少通过协议机制来讲述报头的每个数据结构定义的目的。以下将会具体来说明这些机制的作用。
3.1 确认应答(ACK)机制
TCP 将每个字节的数据都进行了编号. 即为序列号. 每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
我们可以把序号暂时理解成一维数组,数组的最后一位下标就是序号
序号的作用
- 确认应答
- 按序到达
- 去重
丢包
在特定的时间间隔内,收不到应答,判定为报文丢了-》收不到应答&&超时(等待时间式随情况变化的)
- 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;
- 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发(如果对方已经收到了,重发后则根据序号去重);
但是, 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了;
在很多教材和说明里面这种情况也被称为丢包,但是发送方没有收到ACK只能意味数据可能丢失对方没收到,无法百分百保证对方收到消息,用无法保证可靠性来形容这种情况更为合适!!
附:超时时间的确定
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返
回”. - 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算(探索)这个最大超时时间.
- Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控
制, 每次判定超时重发的超时时间都是 500ms 的整数倍.- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
3.2 连接管理机制(理解为什么需要连接的重要部分)
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接
其中操作系统发消息和我们connect等函数调用是分层的
服务端状态转化:
- [CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连
接; - [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入
内核等待队列中, 并向客户端发送 SYN 确认报文. - [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入
ESTABLISHED 状态, 可以进行读写数据了. - [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close), 服务
器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT; - [CLOSE_WAIT -> LAST_ACK] 进入 CLOSE_WAIT 后说明服务器准备关闭连
接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN) - [LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK, 彻底关闭连接.
客户端状态转化
- [CLOSED -> SYN_SENT] 客户端调用 connect, 发送同步报文段; • [SYN_SENT -> ESTABLISHED] connect 调用成功, 则进入 ESTABLISHED 状
态, 开始读写数据; - [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用 close 时, 向服务器发送结
束报文段, 同时进入 FIN_WAIT_1; - [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进
入 FIN_WAIT_2, 开始等待服务器的结束报文段; - [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入
TIME_WAIT, 并发出 LAST_ACK; - [TIME_WAIT -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文
最大生存时间)的时间, 才会进入 CLOSED 状态.
等待2MSL的原因
- 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服
务器立刻重启,对应报文还在网络中传输, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么
服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然可以重发 LAST_ACK);
TIME_WAIT 状态会引起 bind error ,站在应用角度,我们既要TIME_WAIT又要能够立即重启。
使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符
理解确认连接
为什么要进行三次握手????
男女朋友->成为夫妻需要满足
- 双方愿意
- 父母等外部条件满足
三次握手的原因(以客户端向服务器发为走向): 1.以最小成本,百分百确定双方有通信意愿。 第一次握手,服务器知道自己能收消息,但不知道自己能发消息,客户端什么也不知道。第二次握手客户端知道自己能收发消息,而服务器依旧只能知道自己收消息。第三次握手双方都确定有了自己收发消息的能力 2.可以验证网络是通畅的,确认双方都有全双工的能力(以最短的方式,验证全双工)
此外三次握手也是四次握手,在第二次握手的时候,服务端执行了两个动作,ACK和SYN,只不过服务端无脑答应客户端连接请求,携带ACK的报文同时携带了SYN,捎带应答使四次握手压缩为三次握手。
断开连接:
进行四次挥手活动,建立断开连接的共识。在应用层的表现就是ciose网络文件
那么为什么断开连接不能像三次握手那样把FIN和ACK压缩为一个呢?因为在大部分情况下,当客户端想要断开连接的时候,服务端不一定想要断开连接!它的发送缓冲区可能还有数据,因此当客户端断开连接的时候,可能只是关闭了文件的写,而读可能依旧开着,执行单双工模式。
先发起断开连接的一端,最后会进入TIME_WAIT状态,即使完成四次挥手
3.3 滑动窗口
滑动窗口的本质是流量控制,TCP传输时面向字节流的
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个 ACK 确认应答. 收到 ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口
大小就是 4000 个字节(四个段).
- 发送前四个段的时候, 不需要等待任何 ACK, 直接发送;
- 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪
些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉; - 窗口越大, 则网络的吞吐率就越高;
细节解释理解:
滑动窗口的存在我们可以理解为在发送缓冲区这个一个数组中的两个下标组成的闭合区域,窗口左边的区域数据我们可以确定为已经发送的数据(无效数据),由此便可以不需要可以删除清空数据,右边的区域则是等待发送或者无数据区域又或者两者皆有。
序号发送的轮次依次增大,意味滑动窗口基本向右滑动(宏观),而滑动的本质就是下标改变。
滑动窗口的大小由谁决定呢?由对方接收的能力决定,通过收到对方报文win大小来决定自己的窗口大小
而丢包问题的底层解决基础就是滑动窗口
解决丢包问题有两个机制:超时重传和快重传,其中超时重传为兜底。
那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论.
情况一: 数据包已经抵达, ACK 被丢了.
这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认(至于接受方怎么“感知丢失”,则是通过维护一个关键变量 rcv_nxt(Next Receive Sequence Number,下一个期望接收的字节序号);
情况二: 数据包就直接丢了.
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的 ACK, 就像是在提醒
发送端 “我想要的是 1001” 一样; - 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因为 2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 “高速重发控制”(也叫 “快重传”).
假设场景:发送方依次发送 4 个数据包 P1(序号 100-199)、P2(200-299)、P3(300-399)、P4(400-499),其中 P2 在传输中丢失。
步骤 1:接收方收到失序数据包,发送重复 ACK
接收方首先收到 P1(序号 100-199),此时期望序号 rcv_nxt (内核中维护的一个数据结构)更新为 200,发送 ACK1(确认号 = 200)。
由于 P2 丢失,接收方接下来收到 P3(序号 300-399),其序号(300)> 期望序号(200),判定为 “失序”。
接收方暂存 P3(不提交给应用层,等待 P2 补全),rcv_nxt 仍保持 200。
发送重复 ACK2(确认号 = 200),告知发送方 “我需要 200 开始的数据包,300-399 已收到但不连续”。
接收方继续收到 P4(序号 400-499),仍失序(400 > 200),再次发送重复 ACK3(确认号 = 200)。
若发送方继续发送 P5(500-599),接收方收到后发送重复 ACK4(确认号 = 200)。
步骤 2:发送方累计重复 ACK,触发快速重传
发送方收到 ACK2(重复,确认号 = 200)→ 计数 = 1。
收到 ACK3(重复,确认号 = 200)→ 计数 = 2。
收到 ACK4(重复,确认号 = 200)→ 计数 = 3,达到阈值。
发送方推断:“接收方连续 3 次需要 200,说明 P2(200-299)已丢失”,立即重传 P2(无需等待超时计时器)。
步骤 3:接收方确认重传的数据包,恢复正常传输
接收方收到重传的 P2(200-299),此时 P2、P3、P4、P5 形成连续序号(100-599)。
接收方更新 rcv_nxt 为 600,将所有数据提交给应用层,发送 ACK5(确认号 = 600)。
发送方收到 ACK5,确认 P2 及后续数据包均已被接收,重置重复 ACK 计数,继续正常发送数据。
3.4 拥塞控制
网络丢包问题的多少所造成的结论也是不同的,当出现大面积丢包的时候,这个问题就成为网络拥塞问题。
在网络硬件无障碍的前提下,TCP除了考虑双方主机问题,也考虑了网络问题。
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
- 此处引入一个概念称为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为 1;
- 每次收到一个 ACK 应答, 拥塞窗口加 1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较
小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快.
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增
长,目的是不断探测网络新的拥塞的值。
- 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
不是由滑动窗口来决定传输数据多少嘛?滑动窗口大小=对方接受能力大小。因此为了支持上面的拥塞控制算法,引入了拥塞窗口
拥塞窗口:一个临界值,值以内,网络大概率不拥堵,值以上,网络可能拥堵
滑动窗口=min(对方win,拥塞窗口)!!谁小谁是主要矛盾
3.5 延迟应答
延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.
- 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口
就是 500K; - 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费
掉了; - 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也
能处理过来; - 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的
窗口大小就是 1M; 一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是; - 数量限制: 每隔 N 个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms;
3.6 异常问题
- 进程终止: 进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别.
- 机器重启: 和进程终止的情况相同.
- 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
- 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接.