传输层
负责数据能够从发送端传输接收端.
1 再谈端口号
端⼝号(Port)标识了⼀个主机上进⾏通信的不同的应⽤程序;

在TCP/IP协议中,⽤"源IP","源端⼝号","⽬的IP","⽬的端⼝号","协议号"这样⼀个五元组来标识⼀个 通信(可以通过netstat-n查看);
端⼝号范围划分
- 0-1023:知名端⼝号,HTTP,FTP,SSH等这些⼴为使⽤的应⽤层协议,他们的端⼝号都是固定的
- 1024-65535:操作系统动态分配的端口号.客户端程序的端⼝号,就是由操作系统从这个范围分配 的.
认识知名端⼝号(Well-KnowPortNumber)
有些服务器是⾮常常用的,为了使用⽅便,⼈们约定⼀些常⽤的服务器,都是⽤以下这些固定的端⼝号: • ssh服务器,使⽤22端口
• ftp服务器,使⽤21端口
• telnet服务器,使⽤23端口
• http服务器,使⽤80端口
• https服务器,使⽤443
我们⾃⼰写⼀个程序使⽤端⼝号时,要避开这些知名端⼝号. 两个问题
-
⼀个进程是否可以bind多个端口号?
-
⼀个端⼝号是否可以被多个进程bind?
2 UDP协议
UDP/IP/TCP的报头是二进制的
http的报头是文本格式的
UDP协议端格式


长度: 整个数据报(报头 + 载荷)的长度 0-65535 ,最大是64kb,报头长度是8字节(一个2字节)
- 最小长度 :8 字节(仅包含 UDP 头部,无数据)
- 最大长度 :65535 字节 (头部 8 字节 + 数据 65527 字节)
- 这由 "长度" 字段的 16 位二进制限制决定(2¹⁶-1=65535)
校验和:CRC

但是这样的校验方法是有一点小问题的,有可能校验和一样,但是数据变了,前面的bit小一点,后面的大一点,让他的值一样
注意:但是这样发生的机率比较小
UDP的特点
UDP传输的过程类似于寄信.
• ⽆连接:知道对端的IP和端⼝号就直接进⾏传输,不需要建⽴连接;
• 不可靠:没有确认机制,没有重传机制;如果因为⽹络故障该段⽆法发到对⽅,UDP协议层也不会给应 ⽤层返回任何错误信息;
• ⾯向数据报:不能够灵活的控制读写数据的次数和数量;
⾯向数据报
应⽤层交给UDP多⻓的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据:
• 如果发送端调⽤⼀次sendto,发送100个字节,那么接收端也必须调⽤对应的⼀次recvfrom,接收100 个字节;而不能循环调用10次recvfrom,每次接收10个字节
UDP使用注意事项
我们注意到,UDP协议⾸部中有⼀个16位的最大长度.也就是说⼀个UDP能传输的数据最大长度是 64K(包含UDP⾸部). 然而64K在当今的互联⽹环境下,是⼀个非常小的数字. 如果我们需要传输的数据超过64K,就需要在应⽤层⼿动的分包,多次发送,并在接收端⼿动拼装;
基于UDP的应⽤层协议:
• NFS:网络文件系统
• TFTP:简单⽂件传输协议
• DHCP:动态主机配置协议
• BOOTP:启动协议(⽤于⽆盘设备启动)
• DNS:域名解析协议 当然,也包括你⾃⼰写UDP程序时⾃定义的应⽤层协议;
3 TCP协议
TCP全称为"传输控制协议(TransmissionControlProtocol").⼈如其名,要对数据的传输进⾏⼀个详细 的控制;
TCP协议段格式

**16 位源端口号 / 目的端口号:**区分同一设备上的不同应用程序(比如浏览器用 80 端口,微信用其他端口),源端口是发送方应用的端口,目的端口是接收方应用的端口。
32 位序号: TCP 是 "字节流" 协议,这个序号代表本报文段中第一个数据字节的编号,用来解决数据 "乱序" 问题,让接收方能按正确顺序拼接数据。
32 位确认序号:只有当 "ACK 控制位" 为 1 时 有效,代表接收方期望收到的下一个字节的编号(相当于告诉发送方:"我已经收到了到这个编号 - 1 的数据"),是 "确认应答" 机制的核心字段。
4 位首部长度表示 TCP 报头的长度(单位是 4 字节),因为报头可能包含 "选项",所以用这个字段确定 "报头结束、载荷开始" 的位置。
**选项:**可有可无,因此导致报头的长度是不固定的
注意:TCP的单位 不是以字节为单位的,是以4字节位单位的
一行32个bit ,4个字节,除去选项,固定部分的就是4*5 = 20 个字节
保留位(6):TCP的设计者考虑到了UDP的长度不够,又不能扩展的问题,就再报头中设计了'保留位'
◦URG (Urgent Pointer) - 紧急指针有效标志
◦ ACK (Acknowledgment) - 确认标志
◦ PSH (Push) :提⽰接收端应⽤程序立刻从TCP缓冲区把数据读⾛
◦RST (Reset) :对方要求重新建立连接;我们把携带RST标识的称为复位报⽂段
◦ SYN (Synchronize) :请求建立连接;我们把携带SYN标识的称为同步报⽂段
◦FIN (Finish):通知对方,本端要关闭了,我们称携带FIN标识的为结束报⽂段
六个标志位:TCP最核心的六个标识位
十六位校验和:检验数据是否出错误
16位校验和:发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含 TCP⾸部,也包含TCP数据部分.
16位紧急指针:标识哪部分数据是紧急数据;
**40字节头部选项:**暂时忽略;
TCP第一个核心机制:确认应答

TCP将每个字节的数据都进行了编号.即为序列号

有序号就可以很好的应对"先发后至"的问题
引入序号后,就可以很好的根据序号对数据进行排序。
TCP需要处理后发先至的问题,确保应用程序,通过socket api读取到的数据顺序是正确的

"序号、确认序号都是针对载荷的" 的含义
- 序号 :TCP 报头里的 "32 位序号",指的是本报文段载荷部分的第一个字节的编号(不是报头的字节);
- 确认序号 :TCP 报头里的 "32 位确认序号",指的是期望收到的下一个载荷字节的编号(同样只针对载荷,和报头无关)。
TCP会安排收到这一端安排一个到"缓冲区"(操作系统内核,内存)
通过网卡读到这里的数据,先放到接收缓冲区中,后续代码里调用read ,也是从这里接受缓冲区来读的,序号没有排序完整,read 就阻塞,等待排序完整

- TCP 是可靠传输协议,会自动处理数据的顺序、重传等逻辑,因此基于 TCP 编写代码时,无需手动实现数据排序、组包等逻辑,开发更简便;
- UDP 是不可靠传输协议,若基于 UDP 实现数据传输,需开发者自行处理拆包、组包、排序等逻辑。
第二个核心机制:超时重传
超时重传

- 主机A发送数据给B之后,可能因为⽹络拥堵等原因,数据⽆法到达主机B;
- 如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答,就会进⾏重发; 但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;

因此主机B会收到很多重复数据.那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉.
这时候我们可以利⽤前⾯提到的序列号,就可以很容易做到去重的效果.
那么,如果超时的时间如何确定?
• 最理想的情况下,找到⼀个最⼩的时间,保证"确认应答⼀定能在这个时间内返回". • 但是这个时间的⻓短,随着⽹络环境的不同,是有差异的.
• 如果超时时间设的太长,会影响整体的重传效率;
• 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能⽐较⾼性能的通信,因此会动态计算这个最大超时时间.
• Linux中(BSDUnix和Windows也是如此),超时以500ms为⼀个单位进⾏控制,每次判定超时重发的 超时时间都是500ms的整数倍.
• 如果重发⼀次之后,仍然得不到应答,等待2*500ms后再进⾏重传.
• 如果仍然得不到应答,等待4*500ms进⾏重传.依次类推,以指数形式递增.
• 累计到⼀定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接.
1. 拥塞的原因
当路由器 / 交换机处于繁忙状态时,待转发的数据量会超出其硬件支持的转发能力上限,类似 "马路路口的车流量超过了最大通行容量"。
2. 拥塞的后果
- 传输延迟增加:数据需等待设备处理,传输耗时变长,对应现实中的 "堵车";
- 数据包丢弃:若设备的接收缓冲区被占满,无法处理新数据时,会丢弃多余数据包 ------ 通常优先丢弃最新数据,因为网络数据包具有时效性,过时数据的实际价值较低。
优先丢弃新的数据:队列里的 "旧数据" 已经在排队等待处理 ------ 此时这些旧数据还没超过时效(如果已经过时,设备会主动丢弃队列里的过时包);而刚到达的 "最新数据" 如果被加入队列,需要等待前面所有旧数据处理完成才能轮到它,这个等待时间会远超数据包的时效,等处理完时它已经失效了。
3. 网络丢包率的实际体验影响
即便网络丢包概率仅为 10%(90% 的数据可到达),也会严重影响实时应用(如《英雄联盟》等游戏)的体验,导致操作卡顿、画面延迟(即 "卡成 PPT")。
4. TCP 超时阈值的动态调整逻辑
当 A 向 B 传输数据发生超时后,TCP 会延长超时时间阈值;若后续持续超时,阈值会继续延长,但调整并非无限制:当超时次数或等待时间达到临界值,会判定网络出现严重故障,放弃本次传输。
5. 重传的效果与边界
- 重传操作可提升数据成功到达接收方的概率;
- 若多次重传仍失败,说明当前网络丢包率已极高、处于严重故障状态,继续重传的实际意义较小,因此不会持续高频重传。
TCP 重复数据去重机制的流程:
一、传输场景与问题
- 传输过程:主机 A 向主机 B 发送 "数据(1~1000)",B 成功接收该数据,但 B 返回的 "确认应答(下一个是 1001)" 发生丢包;
- 问题产生:主机 A 因未收到确认,在特定时间间隔后重传 "数据(1~1000)",导致主机 B 收到两份相同的数据;
- 潜在风险:若 TCP 不处理,应用层会读取到重复数据(如扣款等关键操作,可能出现重复执行的异常)。
二、TCP 的去重解决机制
TCP 通过 "接收缓冲区 + 序号校验" 实现去重,流程为:
- 主机 B 收到数据后,依据数据的序号(此处为 1~1000),在自身的接收缓冲区中查询该序号段的数据是否已存在;
- 若该序号段数据已存在(即重复数据),则直接丢弃此数据;
- 若该序号段数据不存在,则将数据存入接收缓冲区,后续交付给应用层。
TCP可靠的原因是:超时重传和确认应答
第三个核心机制:连接管理(三次握手)

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

从上面的图看,其实是四次,但是他这个B收到A传来的同步报文段,发送一个ack确认答应和sync 同步报文段其实就可以看作是一个
所以是"三次握手"

⚽ 建⽴连接的意义:
-
投⽯问路,确认当前通信路径是否畅通.
-
协商参数,通信双⽅共同确认⼀些通信中的必备参数数值
下图是TCP状态转换的⼀个汇总:

• 较粗的虚线表示服务端的状态变化情况;
• 较粗的实线表示客⼾端的状态变化情况;
• CLOSED是⼀个假想的起始点,不是真实状态
TIME_WAIT
想⼀想,为什么是TIME_WAIT的时间是2MSL?
• MSL是TCP报⽂的最大⽣存时间,因此TIME_WAIT持续存在2MSL的话
• 就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启,可能会收到来自上⼀个进程的迟到的数据,但是这种数据很可能是错误的);
• 同时也是在理论上保证最后⼀个报文可靠到达(假设最后⼀个ACK丢失,那么服务器会再重发⼀个 FIN. 这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK);
CLOSE_WAIT
⼀般而言,对于服务器上出现大量的CLOSE_WAIT状态,原因就是服务器没有正确的关闭socket,导致 四次挥⼿没有正确完成.这是⼀个BUG.只需要加上对应的close即可解决问题
注意事项:
1. 初始序号的核心定义
它是 TCP 连接建立阶段(三次握手)双方协商的关键信息,用于标记后续数据报文的起始编号,是 TCP 实现 "数据有序传输、去重、流量控制" 的基础。
2. 不从 0 开始的原因
- 避免历史数据干扰:若固定从 0 开始,前一次连接中延迟到达的旧数据包,可能被新连接误认为是有效数据,导致数据错误。
- 提升安全性:固定初始值易被攻击者预测,进而伪造数据包(如 TCP 序列号预测攻击);随机化 ISN 可降低此类风险。
3. 两次连接 ISN 差异大的原因
TCP 协议规定,ISN 需基于时间戳等动态参数生成(通常每 4 微秒递增 1),因此不同时间建立的连接,ISN 会随时间显著变化,既保证了序号的唯一性,也进一步强化了安全性与抗干扰能力。
四次挥手:

四次挥手:有的时候可以合成一个,有的时候不行(就是延时应答)

一、先明确:四次挥手的基础流程
正常四次挥手是主动关闭方(如客户端)与被动关闭方(如服务端)的分步骤关闭:
- 客户端发
FIN:标记 "我没有数据要发了,准备关闭我的发送连接"; - 服务端发
ACK:确认 "收到你的 FIN 请求"; - 服务端发
FIN:标记 "我也没有数据要发了,准备关闭我的发送连接"; - 客户端发
ACK:确认 "收到你的 FIN 请求"。
二、能合并为 "三次挥手" 的条件
当被动关闭方(服务端)收到 FIN 时,已经没有待发送的数据,此时服务端可以将 "步骤 2 的 ACK" 和 "步骤 3 的 FIN"** 合并为一个报文(FIN+ACK)** 发送,从而将四次挥手简化为三次:
- 客户端发
FIN; - 服务端发
FIN+ACK(合并了 ACK 和 FIN); - 客户端发
ACK。
三、无法合并(必须四次)的场景:延时应答
当被动关闭方(服务端)收到 FIN 时,还有未发送的数据,此时必须执行 "延时应答" 逻辑:
- 先回复
ACK(立即确认客户端的 FIN,避免客户端超时重发); - 延迟发送
FIN:等待自己的剩余数据全部发送完毕、处理完成后,再发送FIN。
此时 "ACK" 和 "FIN" 无法合并(因为中间需要传输数据),所以必须保持四次挥手流程。
总结
- 能合并(三次):被动关闭方无未发数据,可立即合并 ACK+FIN;
- 不能合并(四次):被动关闭方有未发数据,需先 ACK、后发完数据再 FIN(即延时应答场景)。
流程:
- 两端角色
- 左侧 :主动关闭方(比如客户端),主动发起连接关闭;
- 右侧 :被动关闭方(比如服务器),被动响应关闭请求。
- 步骤拆解(按流程顺序)
步骤 1:主动关闭方发起关闭
- 左侧(主动方)调用
close(fd)触发连接关闭,此时左侧进入FIN_WAIT_1状态; - 同时向右侧(被动方)发送 FIN 报文(表示 "我没有更多数据要发送了")。
步骤 2:被动关闭方进入 CLOSE_WAIT
- 右侧(被动方)收到 FIN 报文后,进入
CLOSE_WAIT状态; - 同时向左侧回复 ACK 报文(确认 "已收到你的关闭通知");
- 此时右侧的业务逻辑还在执行:通过
read(connfd, buf, size)阻塞等待客户端的数据请求(直到read返回 0,才知道对方已停止发送数据)。
步骤 3:主动关闭方进入 FIN_WAIT_2
- 左侧(主动方)收到右侧的 ACK 报文后,进入
FIN_WAIT_2状态,等待右侧发送自己的 FIN 报文。
步骤 4:被动关闭方发起 FIN
- 右侧(被动方)的
read返回 0(表示 "对方已无数据发送"),于是调用close(connfd); - 调用后右侧向左侧发送 FIN 报文 (表示 "我也没有更多数据要发送了"),此时右侧进入
LAST_ACK状态。
步骤 5:主动关闭方进入 TIME_WAIT
- 左侧(主动方)收到右侧的 FIN 报文后,回复 ACK 报文(确认 "已收到你的关闭通知");
- 回复后左侧进入
TIME_WAIT状态(需等待2 倍 MSL(报文最大生存时间),防止最后一个 ACK 丢失导致右侧重发 FIN)。
步骤 6:两端最终关闭
- 左侧(主动方)在
TIME_WAIT超时后,进入CLOSED状态; - 右侧(被动方)收到左侧的 ACK 报文后,进入
CLOSED状态。
核心状态说明
- CLOSE_WAIT :被动关闭方的中间状态,含义是 "对方已停止发送数据,但我还可以继续发数据",直到自己调用
close才会发送 FIN; - TIME_WAIT:主动关闭方的最后状态,是 TCP 的 "可靠性兜底",避免最后一个 ACK 丢失导致连接关闭不彻底。
- 支撑 TIME_WAIT 状态的设计 :TCP 主动关闭方的
TIME_WAIT状态需等待2 倍 MSL (即2*MSL),原因是:- 确保最后一个 ACK 报文能到达被动关闭方(若 ACK 丢失,被动方会在 1 个 MSL 内重传 FIN,主动方(仍在 TIME_WAIT)可重新发送 ACK);
- 确保旧连接的所有报文都从网络中消失,彻底避免旧报文的干扰。
例子必要性:(TIME_WAIT)
- 假设 A 收到 B 的 FIN 后,回了 ACK 但直接释放连接(不进入 TIME_WAIT);
- 若这个 ACK 丢失,B 会重传 FIN;
- 但 A 已释放连接,无法处理重传的 FIN,导致 B 的连接无法正常关闭。

MSL(Maximum Segment Lifetime):最大的报文传送时间,默认是60s,可以修改
TCP 四次挥手的异常场景
服务器(被动关闭方 B)因代码逻辑 bug,始终未调用close。
双方状态与行为
-
主动方(A)的行为:A 发送 FIN(发起关闭)后,长时间未收到 B 的后续挥手报文(B 的 FIN),会主动释放连接,清除 B 的相关信息,结束连接流程。
-
服务器(B)的状态 :B 收到 A 的 FIN 后,已进入
CLOSE-WAIT状态(但因未调用close,不会发送自己的 FIN);此时 B 会持续保存连接的相关信息,但无法进行正常数据通信,连接处于 "僵死" 状态。
第四个核心机制:滑动窗口
刚才我们讨论了确认应答策略,对每⼀个发送的数据段,都要给⼀个ACK确认应答.收到ACK后再发送下 ⼀个数据段.这样做有⼀个⽐较⼤的缺点,就是性能较差.尤其是数据往返的时间较⻓的时候.

既然这样⼀发⼀收的⽅式性能较低,那么我们⼀次发送多条数据,就可以⼤⼤的提⾼性能(其实是将多个 段的等待时间重叠在⼀起了).

• 窗⼝大小指的是无需等待确认应答⽽可以继续发送数据的最⼤值.上图的窗⼝⼤⼩就是4000个字节 (四个段).
• 发送前四个段的时候,不需要等待任何ACK,直接发送;
• 收到第⼀个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
• 操作系统内核为了维护这个滑动窗⼝,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只 有确认应答过的数据,才能从缓冲区删掉;
• 窗⼝越大,则网络的吞吐率就越高

只有确认1001-2000收到了才会发送这个2001-3000,所以我提前收到了2001-3000,就可以直接确认收到了2001-3000

那么如果出现了丢包,如何进⾏重传?这⾥分两种情况讨论.
情况⼀:数据包已经抵达,ACK被丢了

B接收:
TCP 接收方(这里是主机 B,即服务器)的 ACK "下一个是 3001",只有在它确实收到了 "1~3000 的连续数据段" 时,才会发送。
如果接收方(B)没收到前面的段(比如 2001~3000 丢了),即使后面的段(3001~4000)先到达:
- 接收方会缓存 3001~4000,但不会发送 "下一个是 3001" 的 ACK;
- 它只会持续发送 "下一个是 2001" 的 ACK(代表已收到的连续数据是 1~2000,2001~3000 缺失)。
A接收:
接收方 B 已经完整收到了 1~3000 的连续数据段,也发了 "ACK=3001" 告诉 A"我收到 1~3000 了",但这个 ACK 在发给 A 的过程中丢包了;之后 B 又收到了 A 发的 3001~4000,此时 A 可以直接从 4001 继续发送了但是 1~3000丢包了,A没有收到收到二零3001-4000 ------ 因为 B 既然能处理 3001~4000,说明 B 肯定已经收到 2001~3000 了,所以A可以直接发送4001了,默认B收到2001-3000
场景描述:
-
初始数据传输:
-
主机A 向 主机B 发送数据,数据是按块进行划分的。例如,A先发送1~1000的数据块,B收到后会返回一个确认(ACK),告知A自己已经接收到1~1000的数据。
-
A继续发送下一个数据块,依次是1001~2000,2001~3000,依此类推。
-
当 主机B 完全接收到1~3000的连续数据时,它会发送 ACK=3001,意味着它已经收到数据1~3000,接下来期望的数据是从3001开始。
-
-
ACK丢包:
- 但是,ACK=3001 在传输过程中丢失了。这意味着 主机A 并没有收到来自主机B的确认,因此它不知道主机B已经成功接收到1~3000的数据。
-
主机A继续发送数据:
-
主机A 没有收到ACK=3001,于是它继续发送下一个数据块,数据段为3001~4000,主机B成功接收到了这些数据。
-
主机A 此时发现 主机B 能够处理3001~4000的数据,表明主机B已经收到了2001~3000的数据(因为B不可能接收3001~4000而没有收到2001~3000)。
-
-
主机A的默认处理:
-
由于主机B能够处理3001~4000的数据块,主机A就推测 主机B已经收到了2001~3000的数据,因此A可以直接从4001开始继续发送数据,而不需要重新发送2001~3000。
-
这种情况表明,即使没有收到明确的ACK确认,主机A仍然会依赖 累积确认机制(即假设接收方B已经按顺序收到前面的所有数据),并跳过了可能已经丢失的ACK消息。
-
TCP协议中的累积确认:
在 TCP协议 中,ACK是累积的 ,这意味着每个ACK不仅表示某一块数据已经接收成功,还间接确认了前面所有的数据块已经接收到。所以,即便 ACK=3001 丢失了,主机A也能通过 主机B处理3001~4000的数据 来推测前面的数据已经被接收并确认。
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进⾏确认;
情况⼆:数据包就直接丢了

• 当某⼀段报⽂段丢失之后,发送端会⼀直收到1001这样的ACK,就像是在提醒发送端"我想要的是 1001" ⼀样;
• 如果发送端主机连续三次收到了同样⼀个"1001"这样的应答,就会将对应的数据1001-2000重新 发送;
• 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就 已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为"⾼速重发控制"(也叫"快速重传").
超时重传和快速重传
- 超时重传的典型场景:传输数据量少、批量传输不明显时,丢包后无足够后续数据段产生重复 ACK,只能等待 RTO 超时后重传;
- 快速重传的典型场景:传输数据量多、滑动窗口批量传输时,丢包后易出现失序报文,从而产生重复 ACK,无需等待超时即可触发重传。

滑动窗口是默认机制,数据量决定了 "是否易产生重复 ACK",进而影响重传方式 ------ 超时重传是 "无重复 ACK 时的兜底",快速重传是 "有重复 ACK 时的优化",二者协同保障 TCP 的可靠传输。