传输层作为OSI七层模型和TCP/IP四层模型中的核心环节,负责将数据从发送端可靠、高效地传输到接收端。在这一层中,TCP(传输控制协议)和UDP(用户数据报协议)是两种最核心的协议,它们以截然不同的设计理念,支撑着各类网络应用的通信需求------TCP如同一位严谨的"管家",通过复杂的机制确保数据的可靠交付;UDP则像一位高效的"信使",以简洁直接的方式实现快速传输。接下来,我们将深入剖析这两种协议的特性与运作方式,理解它们如何在传输层各司其职,适配不同场景下的通信需求。
1. 端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序

在 TCP/IP 协议中,用 "源 IP","源端口号","目的 IP","目的端口号","协议号" 这样一个五元组来标识一个通信(可以通过 netstat -n 查看)

端口号范围划分
0 - 1023:知名端口号,HTTP,FTP,SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。
1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
固定的端口号:
ssh 服务器, 使用 22 端口
ftp 服务器, 使用 21 端口
telnet 服务器, 使用 23 端口
http 服务器, 使用 80 端口
https 服务器, 使用 443
执行下面的命令, 可以看到知名端口号
bash
cat /etc/services

我们自己写一个程序使用端口号时,要避开这些知名端口号。
2.UDP 协议
UDP(User Datagram Protocol,用户数据报协议)是 TCP/IP 协议族中一种无连接的传输层协议,主要用于需要高效传输但对可靠性要求不高的场景。
(1)UDP****协议端格式

UDP 数据包由 8 字节的报头和有效载荷字段构成。报头由 4 个域组成,每个域占 2 个字节。
16 位 UDP 长度:表示整个数据报(UDP 首部+UDP 数据)的最大长度。由于 UDP 报文头长度固定为 8 字节,所以报文长度最小为 8。
校验值:使用二进制反码求和算法,用于检验数据在传输过程中是否被损坏。若校验和出错,接收端会直接丢弃该数据报。
(2)UDP****的特点
UDP 传输的过程类似于寄信。
无连接 :知道对端的 IP 和端口号就直接进行传输,不需要建立连接;
不可靠 :没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量;
(3)面向数据报:
应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并;
用 UDP 传输 100 个字节的数据:
如果发送端调用一次 sendto,发送 100 个字节,那么接收端也必须调用对应的一次 recvfrom,接收 100 个字节;而不能循环调用 10 次 recvfrom,每次接收 10 个字节;
(4)UDP 的缓冲区
UDP 没有真正意义上的 发送缓冲区 。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
UDP 具有接收缓冲区,但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致;如果缓冲区满了,再到达的 UDP 数据就会被丢弃;
UDP 的 socket 既能读,也能写,这个概念叫做 全双工。
(5)UDP 使用注意事项
UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)。
然而 64K 在当今的互联网环境下,是一个非常小的数字,如果我们需要传输的数据超过 64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。
(6)基于 UDP 的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然,也包括你自己写 UDP 程序时自定义的应用层协议。
3.TCP 协议
TCP 全称为 "传输控制协议(Transmission Control Protocol"),该协议会对数据的传输进行一个详细的控制。
3.1 TCP****协议段格式
源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
32 位序号 :发送数据的字节流编号,用于标识当前报文段中第一个数据字节的位置(保证数据有序)
32 位确认号:期望接收的下一个字节的序号,用于确认已收到的数据(仅当 ACK 标志位为 1 时有效)。
4 位 TCP 报头长度:表示 TCP 首部的长度(单位:32 位,即 4 字节),最大值为 15(15×4=60 字节)。
6 位标志位 :
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走
RST:对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
SYN:请求建立连接; 我们把携带 SYN 标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带 FIN 标识的为结束报文段
16 位校验和:发送端填充,CRC 校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含 TCP 首部,也包含 TCP 数据部分。
16 位紧急指针: 标识哪部分数据是紧急数据;
选项字段:最长 40 字节,用于补充功能(如 MSS 协商、窗口扩大因子等)。
3.2 确认应答**(ACK)**机制

保证数据可靠传输。接收端收到数据后,会发送携带确认号的应答报文,告知发送端 "已收到哪些数据 " 以及 "下一次希望接收的数据起始位置"。

示例:发送端发送序号为 1000-2000 的数据,接收端成功接收后,会返回确认号 2001,意为 "已收到 1000-2000,下次请从 2001 开始发"。
注意:确认号仅当 ACK 标志位为 1 时有效(建立连接后,ACK 通常始终为 1)。
3.3 超时重传机制
主机 A 发送数据给 B 之后,可能因为网络拥堵等原因,数据无法到达主机 B;
如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答,就会进行重发;
收不到应答 + 超时 -》发送方确认丢包
但是,主机 A 未收到 B 发来的确认应答,也可能是因为 ACK 丢失了,这时如果主机A重传数据会导致主机 B 会收到很多重复数据。那么 TCP 协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
TCP 为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。
如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。
累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接。
3.4 连接管理机制
在正常情况下,TCP 要经过三次握手建立连接,四次挥手断开连接。
3.4.1 三次握手建立连接
三次握手(Three-way Handshake)是 TCP 建立连接时,客户端和服务器通过三次报文交互确认双方收发能力的过程,目的是同步双方的序列号并协商连接参数
过程详解:
-
第一次握手(客户端 → 服务器)
-
客户端主动发起连接,发送SYN 报文(标志位SYN=1),并生成一个初始序列号seq=x(x 为随机生成的 32 位整数)。
-
此时客户端状态从CLOSED变为SYN_SENT(等待服务器响应)。
-
-
第二次握手(服务器 → 客户端)
-
服务器收到 SYN 报文后,确认客户端请求,回复SYN+ACK 报文:
-
标志位
SYN=1
(表示同意建立连接,同步服务器自身的序列号); -
标志位
ACK=1
(确认收到客户端的 SYN); -
服务器生成自己的初始序列号
seq=y
; -
确认号
ack=x+1
(表示已收到客户端的序列号 x,下一次期望接收 x+1)。
-
-
此时服务器状态从
LISTEN
变为SYN_RCVD
(等待客户端确认)。
-
-
第三次握手(客户端 → 服务器)
-
客户端收到 SYN+ACK 报文后,发送ACK 报文:
-
标志位
ACK=1
(确认收到服务器的 SYN); -
序列号
seq=x+1
(基于客户端初始序列号递增,因已发送 1 个字节的 SYN); -
确认号
ack=y+1
(表示已收到服务器的序列号 y,下一次期望接收 y+1)。
-
-
客户端状态从SYN_SENT变为ESTABLISHED(连接建立,可发送数据);
-
服务器收到 ACK 后,状态从SYN_RCVD变为ESTABLISHED,双方开始数据传输。
-
核心目的 :防止 "已失效的连接请求报文" 突然到达服务器,导致错误连接。
例如:客户端发送的第一个 SYN 报文因网络延迟滞留,客户端超时后重新发送 SYN 并建立连接;若滞留的 SYN 随后到达服务器,服务器会误以为是新连接请求,若仅两次握手,服务器会直接建立连接并分配资源,导致资源浪费。
三次握手通过客户端的第三次 ACK,确保双方均确认 "对方已准备好",以最短的方式验证了**全双工,**也就是验证了收发双方所处的网络是通畅的,能够支持全双工。
3.4.2 四次握手断开连接
四次挥手(Four-way Wavehand)是 TCP 断开连接时,双方通过四次报文交互确认数据传输完毕,并释放资源的过程,因半关闭特性需分步骤完成。
过程详解:
假设客户端主动发起关闭(实际任何一方都可主动关闭):
-
第一次挥手(主动关闭方 → 被动关闭方)
-
主动关闭方(如客户端)完成数据发送后,发送FIN 报文 (标志位
FIN=1
),序列号seq=u
(u 为最后一次发送数据的序列号 + 1)。 -
主动关闭方状态从
ESTABLISHED
变为FIN_WAIT_1
(等待对方确认)。
-
-
第二次挥手(被动关闭方 → 主动关闭方)
-
被动关闭方(如服务器)收到 FIN 后,回复ACK 报文 (标志位
ACK=1
),确认号ack=u+1
,序列号seq=v
(v 为服务器最后一次发送数据的序列号 + 1)。 -
此时被动关闭方进入
CLOSE_WAIT
状态(表示 "收到关闭请求,正在处理剩余数据"),主动关闭方收到 ACK 后进入FIN_WAIT_2
状态(等待服务器的 FIN)。
-
-
第三次挥手(被动关闭方 → 主动关闭方)
-
被动关闭方处理完所有数据后,发送FIN 报文 (标志位
FIN=1
),序列号seq=w
(w 为 v+1,因第二次挥手已发送 1 个 ACK 字节),确认号ack=u+1
(重复确认)。 -
被动关闭方状态从
CLOSE_WAIT
变为LAST_ACK
(等待最后一次 ACK)。
-
-
第四次挥手(主动关闭方 → 被动关闭方)
-
主动关闭方收到 FIN 后,回复ACK 报文 (标志位
ACK=1
),确认号ack=w+1
,序列号seq=u+1
。 -
主动关闭方进入
TIME_WAIT
状态(等待 2 倍 MSL,确保对方收到 ACK),被动关闭方收到 ACK 后直接进入CLOSED
状态。 -
主动关闭方等待
TIME_WAIT
结束后,也进入CLOSED
状态,连接彻底释放。
-
为什么需要四次挥手?
核心原因:TCP 是全双工通信(双方可同时发送数据),关闭连接时需分别确认 "各自的数据已传输完毕"。
第二次挥手仅确认 "收到对方的关闭请求",但被动关闭方可能仍有数据需发送,因此需先回复 ACK,待数据发送完后再发送 FIN(第三次挥手)。
主动关闭方的TIME_WAIT
状态是为了防止最后一个 ACK 丢失:若 ACK 丢失,被动关闭方会重发 FIN,主动关闭方可在TIME_WAIT
期间再次回复,避免对方因未收到 ACK 而一直滞留。

3.4.3 握手过程中的状态转化
(1)服务端状态转化:
-
CLOSED -\> LISTEN\]:服务器端调用 listen 后进入 LISTEN 状态,等待客户端连接;
-
SYN_RCVD -\> ESTABLISHED\]:服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了。
-
CLOSE_WAIT -\> LAST_ACK\]:进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用 close 关闭连接时,会向客户端发送FIN,此时服务器进入 LAST_ACK 状态,等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)。
(2)客户端状态转化:
-
CLOSED -\> SYN_SENT\] 客户端调用 connect,发送同步报文段;
-
ESTABLISHED -\> FIN_WAIT_1\] 客户端主动调用 close 时,向服务器发送结束报文段,同时进入 FIN_WAIT_1;
-
FIN_WAIT_2 -\> TIME_WAIT\] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT,并发出 LAST_ACK;
下图是 TCP 状态转换的一个汇总:
较粗的虚线表示服务端的状态变化情况;
较粗的实线表示客户端的状态变化情况;
CLOSED 是一个假想的起始点, 不是真实状态;

(3)异常情况处理:
服务端:
SYN_RCVD 状态的超时处理:
-
SYN_RCVD -\> LISTEN\]:服务端在 SYN_RCVD 状态下,若长时间未收到客户端对 "SYN+ACK" 的确认报文(ACK),会触发超时重传机制(通常重传 3-5 次 "SYN+ACK")。若重传后仍未收到 ACK,则会释放该半连接,回到 LISTEN 状态(避免半连接队列溢出)。
-
LISTEN -\> CLOSED\]:服务端若主动关闭监听套接字(如调用close关闭监听描述符),会直接从 LISTEN 状态回到 CLOSED 状态,不再接受新的连接请求。
-
ESTABLISHED -\> CLOSED\]:若服务端在 ESTABLISHED 状态下收到客户端发送的 RST 报文(如客户端异常崩溃后重启),会直接释放连接,进入 CLOSED 状态(无需经过四次挥手流程)。
SYN_SENT 状态的异常处理:
-
SYN_SENT -\> CLOSED\]:客户端在 SYN_SENT 状态下,若收到服务端的 RST 报文(如服务端未监听目标端口),会直接放弃连接,回到 CLOSED 状态。
FIN_WAIT_1 状态的特殊跳转(双方同时关闭):
-
FIN_WAIT_1 -\> TIME_WAIT\]:若客户端在 FIN_WAIT_1 状态下,同时收到服务端发送的 FIN 报文(即服务端也在主动关闭连接,双方 "同时挥手"),此时客户端会立即发送对该 FIN 的ACK,并直接进入 TIME_WAIT 状态(无需经过 FIN_WAIT_2)。
-
FIN_WAIT_2 -\> CLOSED\]:客户端在 FIN_WAIT_2 状态下需要等待服务端的 FIN 报文。若长时间未收到(如服务端异常滞留未发送 FIN),客户端会触发超时机制(超时时间由系统配置,通常为几分钟),最终直接关闭连接回到 CLOSED 状态(避免 FIN_WAIT_2 状态永久滞留)。
-
ESTABLISHED -\> CLOSED\]:与服务端类似,客户端在 ESTABLISHED 状态下若收到服务端的 RST 报文(如服务端异常关闭),会直接释放连接,进入 CLOSED 状态。
TIME_WAIT 状态的 2MSL 意义:
客户端在 TIME_WAIT 状态等待 2MSL(报文最大生存时间)的核心原因有两点:
-
确保最后发送的 ACK 能被服务端收到(若服务端未收到 ACK,会重传 FIN,客户端在TIME_WAIT 期间可再次发送 ACK);
-
确保本次连接的所有报文已从网络中消失(避免新连接复用端口时,收到旧连接的残留报文)。
CLOSE_WAIT 状态的注意事项:
- 服务端进入 CLOSE_WAIT 状态后,意味着 "被动关闭" 的开始,此时连接的关闭权完全由应用层控制:若应用层未及时调用close (如代码逻辑遗漏),会导致连接长期滞留于 CLOSE_WAIT 状态,造成系统资源(文件描述符)泄露。
半连接与全连接队列:
服务端在 LISTEN 状态时,会维护两个队列:
半连接队列(SYN 队列):存储已收到 SYN 但未完成三次握手的连接(对应 SYN_RCVD 状态);
全连接队列(Accept 队列):存储已完成三次握手(ESTABLISHED 状态)但未被应用层accept取走的连接。
3.5 滑动窗口
在 TCP 协议的确认应答机制中,若采用逐段确认的策略 ------ 即对每一个发送的数据段都返回一个 ACK 确认应答,且需等待该 ACK 到达后再发送下一个数据段 ------ 虽能确保数据传输的准确性,但存在一个显著缺陷:性能损耗明显。尤其当网络往返时间(RTT)较长时,大量时间被浪费在等待确认的过程中,数据传输效率会大幅降低。
既然这种 "一发一收" 的串行模式会制约性能,那么优化思路便应运而生:通过一次批量发送多个数据段,将多个等待确认的时间窗口重叠起来。这种方式能有效减少因等待 ACK 而产生的空闲时间,从而显著提升整体传输效率。

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

如果出现了丢包,无非最左侧丢失、中间丢失、最右侧报文丢失或这三种情况的组合。
以最左侧丢失为例,如何进行重传? 这里分两种情况讨论。
情况一:数据包已经抵达,ACK 被丢了。

这种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认。因为 ACK 代表着该序列号之前的数据已全部接收完毕。
情况二:数据包就直接丢了

以上图为例,当某一段报文段丢失后,发送端会持续收到编号为1001的ACK,这就如同在提示发送端"我期望接收的是1001"。
如果发送端主机接连三次收到同样的"1001"应答,就会重新发送对应编号为1001 - 2000的数据。
此时,接收端在收到1001后,再次返回的ACK编号就会变为7001。这是因为编号为2001 - 7000的数据,接收端实际上之前就已经收到了,只是被暂存到接收端操作系统内核的接收缓冲区里。
这种机制被称作"高速重发控制",也被叫做"快重传"。
以上两种情况亦有可能触发超时重传来解决:
发送方在发送数据包时,会为每个数据包启动一个超时定时器 。如果在定时器超时之前,没有收到对应的 ACK,就认为该数据包丢失,从而触发超时重传。例如,发送方发送了一个数据包,设置超时时间为 T,如果在时间 T 内没有收到接收方对该数据包的 ACK,就会重传这个数据包。
3.6 流量控制
接收端处理数据的能力是有限度的。要是发送端发送数据的速度太快,使得接收端的缓冲区被填满,此时若发送端继续发送数据,就会造成数据包丢失,进而引发丢包重传等一系列连锁反应。
所以,TCP 协议支持依据接收端的处理能力,来确定发送端的发送速度,这种机制被称为流量控制(Flow Control)。
而滑动窗口的本质就是流量控制的具体实现方案。
-
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段,通 过 ACK 端通知发送端;
-
窗口大小字段越大,说明网络的吞吐量越高;
-
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
-
发送端接受到这个窗口之后,就会减慢自己的发送速度;
-
如果接收端缓冲区满了,就会将窗口置为 0;这时发送方不再发送数据,,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
接收端如何把窗口大小告诉发送端呢?回忆我们的 TCP 首部中,有一个 16 位窗口字段,就是存放了窗口大小信息。
这里有个疑问,16 位数字最大能表示 65535,难道 TCP 窗口的最大大小就是 65535 字节吗?
实际上,在 TCP 首部 40 字节的选项中,还包含了一个窗口扩大因子 M。实际的窗口 大小是窗口字段的值左移 M 位得到的。
3.7 拥塞控制
虽然 TCP 有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机,,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能雪上加霜的。
因此TCP 引入 慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
此处引入一个概念称为拥塞窗口:
-
发送开始的时候,定义拥塞窗口大小为 1;
-
每次收到一个 ACK 应答,拥塞窗口加 1;
例如:
-
第 1 次发送:cwnd=1,发送 1 个报文段;
-
收到 ACK 后,cwnd=2,发送 2 个报文段;
-
再收到 2 个 ACK 后,cwnd=4,发送 4 个报文段;
可见,拥塞窗口的增长是指数级的(1→2→4→8...)。这里的 "慢启动" 仅指初始发送量小,实际增长速度非常快。 -
每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。
像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动" 只是指初始时慢 ,但是增长速度非常快。
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,此处引入一个慢启动的阈值
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
-
当 TCP 开始启动的时候,慢启动阈值等于窗口最大值;
-
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回****1;
少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
- 当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
3.8 其他关键特性与异常处理
3.8.1 延迟应答
延迟应答是 TCP 协议中一种优化窗口利用率的机制,其核心思路是:接收端不立即对收到的数据返回 ACK 应答,而是适当延迟一段时间,待缓冲区有更多空闲空间后再回复,从而尽可能增大反馈的窗口大小,提升整体传输效率。
-
假设接收端缓冲区为 1M,一次收到了 500K 的数据;如果立刻应答,返回的窗口就是 500K;
-
但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了;
-
在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
-
所以如果接收端稍微等一会再应答,那么这个时候返回的 窗口大小就可能是 1M;
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么?肯定也不是:
- 数量限制:每隔 N 个包就应答一次
- 时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,依操作系统不同也有差异;一般 N 取 2,超时时间取 200ms;
3.8.2 捎带应答
在延迟应答的基础上,我们发现很多情况下,客户端服务器在应用层也是 "一发一收" 的。意味着客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine, thank you";
那么这个时候 ACK 就可以搭顺风车,和服务器回应的 "Fine, thank you" 一起回给客户端。
3.8.3 面向字节流
TCP 协议的 "面向字节流" 特性,是指其数据传输过程像 "流动的字节序列" 一样,不刻意区分数据的边界 ,而是通过内核中的缓冲区实现数据的灵活读写与拆分合并,具体表现如下:
核心机制:发送缓冲区与接收缓冲区
当创建一个 TCP socket 时,操作系统内核会为这个连接分配两个缓冲区:
- 发送缓冲区 :应用程序调用write
函数发送数据时,数据不会直接发送到网络,而是先写入发送缓冲区。
- 接收缓冲区 :对方发送的数据到达后,会先存放在接收缓冲区,应用程序通过read
函数从这里读取数据。
这两个缓冲区是 "面向字节流" 的核心支撑,它们使得数据的发送和接收可以灵活调整,无需严格对应单次操作的大小。
数据发送:灵活拆分与合并
发送缓冲区会根据网络情况和数据大小,自动处理数据的拆分或合并:
- 拆分 :如果应用程序一次写入的数据量过大 (超过 TCP 报文段的最大长度,即 MSS),发送缓冲区会将数据拆分成多个符合网络传输要求的小数据包,逐次发送。
- 合并 :如果应用程序多次写入的字节数很少 (例如每次只写 1 个字节),发送缓冲区会先将这些零散数据暂存,等到缓冲区积累到一定长度(或满足超时条件),再合并成一个数据包发送,避免频繁发送小报文浪费网络资源。
数据接收:不依赖发送端的写入方式
接收缓冲区同样体现 "字节流" 的无边界特性:接收端读取数据时,完全不用关心发送端是如何写入的,只需按自己的需求读取任意长度(只要不超过缓冲区中已有的数据量)。
-
发送端可能通过 1 次
write
写入 100 个字节,也可能通过 100 次write
每次写入 1 个字节。 -
接收端既可以通过 1 次
read
读取全部 100 个字节,也可以分 100 次read
每次读取 1 个字节,最终得到的都是完整的 100 字节数据流。
全双工特性
由于每个 TCP 连接同时拥有发送缓冲区和接收缓冲区,通信双方可以在同一时间既发送数据又接收数据,这种双向独立传输的能力称为全双工。例如,客户端可以一边向服务器发送请求,一边接收服务器返回的响应,无需等待单方向传输结束。
3.8.4 粘包问题
在 TCP 通信中,"粘包问题" 是指应用层收到的数据无法清晰区分多个独立的应用层数据包边界 ,导致多个数据包的字节流被 "粘" 在一起的现象。其本质是 TCP "面向字节流" 的特性 与应用层对 "数据包" 的边界需求之间的矛盾。
粘包问题的根源
1. TCP 的字节流特性 :
- TCP 在传输层以字节流形式处理数据,协议头中没有像 UDP 那样的 "报文长度" 字段,仅通过序号保证数据有序 。数据在发送缓冲区可能被合并(小数据)或拆分(大数据),接收缓冲区收到的是连续的字节序列,不保留应用层发送时的 "数据包" 边界。
- 例如:发送端分两次发送 "Hello" 和 "World",接收端可能一次收到 "HelloWorld",无法直接区分这是两个独立的数据包。
2. 应用层的认知差异 :
- 应用层通常需要处理有明确边界的 "数据包"(如一条消息、一个请求),但 TCP 交付给应用层的是无边界的字节流,因此需要应用层自行解决边界识别问题。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。
-
对于定长的包,保证每次都按固定大小读取即可;例如 Request 结构,是固定大小的,那么就从缓冲区从头开始按 sizeof(Request)依次读取即可;
-
对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
-
对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
思考:对于 UDP 协议来说,是否也存在 "粘包问题" 呢?
-
对于 UDP,如果还没有上层交付数据,UDP 的报文长度仍然在。同时,UDP 是一 个一个把数据交付给应用层,就有很明确的数据边界。
-
站在应用层的站在应用层的角度,使用 UDP 的时候,要么收到完整的 UDP 报文,要么不收,不会出现"半个"的情况。
3.8.5 TCP异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没有什么区别
机器重启:和进程终止的情况相同。
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了, 就会进行 reset。即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外,应用层的某些协议,也有一些这样的检测机制。例如 HTTP 长连接中,也会定期检测对方的状态。例如 QQ,在 QQ 断线之后,也会定期尝试重新连接