UDP协议
格式
UDP协议头部格式由8个字节组成,由4个2字节大小的字段组成。
源端口(Source Port,16 位):
- 发送端的端口号,标识数据从哪个端口发出。
- 如果不需要,则可以填 0。
目标端口(Destination Port,16 位):
- 接收端的端口号,表示数据要传输到哪个端口。
长度(Length,16 位):
- UDP 数据报的总长度(包括 UDP 头部和数据部分)。
- 最小值为 8(仅有头部,无数据)。
校验和(Checksum,16 位):
- 用于数据完整性检查,计算方式基于伪首部(Pseudo Header)。
- 如果计算结果为 0,则在 IPv4 下可以填 0;IPv6 必须计算。
UDP是如何完成分用,如何进行解包的?
1.根据报头中的目标端口号,将数据包交给上层。完成分用2.因为UDP报头的大小是固定8字节,报头中还包含数据包的总长度(报头长8字节+数据大小),这样就可以获得数据的大小,完成解包。
根据检测校验和检查 UDP 数据报传输过程中是否发生错误。
添加UDP报头的过程:
- 拷贝应用层数据到
sk_buff
的数据缓冲区。- 在数据前面申请 UDP 头部空间(
head -= sizeof(struct udphdr)
)。- 填充 UDP 头部信息(源端口、目标端口、长度、校验和)。
协议的本质就是结构体。
当我们从该主机的端口号1234发送数据,先到传输层,发送数据的本质就是拷贝数据,找一块缓冲区把数据拷贝下来,再去考虑添加报头。
对应每一个数据报都有对应的struct sk_buff结构体来管理,里面有一个指针指向它的缓冲区,还有两个指针指向数据的开头和结尾。
如何在数据前面添加报头呢?本质就是在数据前面申请一块struct udphdr大小的空间,并填写相关信息。
1.head*指向数据开头,head-=sizeof(struct udphdr) 申请空间
2.(struct udphdr*)head->source=1234; 填写相关信息
(struct udphdr*)head->dest=1235;
...
UDP 的特点
1.无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;
2.不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
3.面向数据报: 不能够灵活的控制读写数据的次数和数量;(如果发送端调用一次 sendto, 发送 100 个字节, 那么接收端也必须调用对应的一次 recvfrom, 接收 100 个字节; 而不能循环调用 10 次 recvfrom, 每次接收 10 个字节) 发几次,收几次
UDP 的缓冲区
发送缓冲区:UDP 没有真正意义上的 发送缓冲区. 调用 sendto 会直接交给内核, 由内核将数
据传给网络层协议进行后续的传输动作。(发完不会进行保存,所以没收到不会重发,只能手动重新发送)
接收缓冲区:UDP 具有接收缓冲区. 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和
发送 UDP 报的顺序一致; 如果缓冲区满了, 再到达的 UDP 数据就会被丢弃;(保存接收到的报文,但不一定按顺序 不可靠)
我们注意到, UDP 协议首部中有一个 16 位的最大长度. 也就是说一个 UDP 能传输的数
据最大长度是 64K(包含 UDP 首部).
然而 64K 在当今的互联网环境下, 是一个非常小的数字.
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包, 多次发送, 并在接收端
手动拼装;(更建议用TCP)
TCP协议
格式
- 源端口号(16位):发送端应用程序的端口号。
- 目标端口号(16位):接收端应用程序的端口号。
- 序列号(32位):表示发送方发送的数据的顺序号,确保数据可以按顺序接收。
- 确认号(32位):如果ACK标志位为1,则表示接收到的数据包的下一个期望字节的序列号。
- 4位首部长度(数据偏移)(4位):指示TCP报文段的头部长度,单位是32位字(4字节)。
- 保留(3位):用于未来的扩展,目前值为0。
- 标志位(9位):标志位用于控制TCP的行为。9个标志位分别为:
标志位 缩写 作用 紧急(Urgent) URG 表示该数据是紧急数据,需优先处理 确认(Acknowledgment) ACK 该数据包携带有效的 ACK 号 推送(Push) PSH 立即交付数据给应用层 复位(Reset) RST 立即重置连接 同步(Synchronization) SYN 用于建立连接(TCP 三次握手) 终止(Finish) FIN 终止连接(TCP 四次挥手) 回显(ECE) ECE 显式拥塞通知 拥塞窗口减少(CWR) CWR 发送方已减少拥塞窗口 非拥塞回显(NS) NS ECN 扩展 - 窗口大小(16位):接收方的接收窗口大小,用于流量控制。
- 校验和(16位):用于保证数据在传输过程中没有被篡改。
- 紧急指针(16位):如果URG标志位为1,则该字段有效,指示紧急数据的偏移。
- 选项(可选,长度可变):可以包含一些附加的TCP选项,如最大段大小(MSS)等。
- 数据(可变长度):TCP报文段的有效载荷部分,包含应用层传输的数据。
4位首部长度(4位):
固定的20字节+选项大小。
4位可以表示的范围[0,15],怎么表示20以上的大小呢?
其实4位首部长度的基本单位是4字节,报头长度=4位首部长度*4。所以最大可表示60
TCP如何完成解包和分用的:
解包:通过4位首部长度,接收端可以计算出数据部分的起始位置,并正确地解包数据。分用:通过目标端口号。
序号和确认序号:
当客户端给服务端发送消息时,怎么知道我的消息有没有发到服务端。服务端再给客户端发个确认信息就好了(只确认收到,一般就只发报头)。但,又怎么确认服务端发的确认信息客户端有没有收到呢?客户端再发个确认确认信息,这样就会进入死循环。
所以没有100%的可靠性,因为总有一条信息是没有确认的。
序号的作用,可以给收到的报文进行排序,按顺序接收,具有可靠性。
确认序号=序号(收到报头中的)+1
比如:我给客户端发消息,报头中序号为10,客户端返回报头中确认序号为11。这就表明客户端收到11序号前的所以报文,并期待收到序号11的报文。
实际上我们并不会发完一条报文,等收到确认报头时,再发下一条。而是一次性发一批报文,客户端再一条一条返回。
况且客户端在会信息时,不一定只发个报头表示收到信息,还有可能带有数据。下面就谈谈为什么要同时存在序号和确认序号。
当客户端收到报文,想发确认和其它信息时,如果只有一个序号,怎么和只确认的进行区分呢?
这就需要存在两个序号,序号和确认序号。
确认序号:告诉对方“我已经收到你发过来的数据,接下来我期待收到哪个字节”。
序号:告诉对方“这是我正在发送的数据,起始字节的位置是这个序号”。
窗口大小:
主机AB相互发报文时,如何判断对方缓冲区还有空间呢?
主机AB缓冲区是否还有空间,只有它们自己最清楚,所以在给对方发报文时,会在报头中填入自己缓冲区剩余的大小,这个字段就是窗口大小。
16位窗口大小能表示最大的字节数为65,535,实际的窗口大小最大就是65535吗?
实际的窗口大小=窗口大小* (2^窗口扩大因子)
窗口扩大因子位于TCP报头选项中的3字节的字段 ,用于增加窗口大小的标度。
(实际窗口大小是 窗口字段的值左移 窗口扩大因子值 位;)
起到流量控制的作用,通过告知接收方缓冲区的剩余空间,帮助维持高效、稳定的数据传输。
1.窗口大小显示自己缓冲区剩余空间的大小,如果剩余空间少可以让对方减少发送报文的速度。
2.反之剩余空间多,可以让对方加快速度,起到流量控制的作用。
为什么TCP报头中没有表示整个报文长度的字段?
TCP 是 面向字节流 的协议。TCP 只提供可靠的、按序的字节流传输,并不会维护单个报文的边界。由于 TCP 只关心数据流的顺序和完整性,而不关心单个数据块的大小,因此没有必要为每个 TCP 报文段专门定义一个总长度字段。
标志位:PSH(清理缓存) PST(重建连接) URG(紧急处理)
1. PSH(Push)标志
作用:
- 用于提示接收方的应用层立即处理当前数据,而不必等待缓冲区填满。
- 发送方设置 PSH 标志后,接收方通常会将数据直接传递给应用程序,而不是先缓冲一部分再处理。
适用场景:
- 交互式应用,如 Telnet、SSH、HTTP 请求等,数据需要尽快传递到应用程序。
- 传输小数据包,如即时消息或按键输入,以减少延迟。
工作机制:
- 当 PSH 置位时,接收方 TCP 协议栈不会等到缓冲区填满,而是立即将数据推送到应用程序。
- 一般来说,TCP 连接关闭时,系统也会自动推送数据,即使 PSH 未设置。
2. RST(Reset)标志
作用:
- 立即终止 TCP 连接,通常用于异常情况或拒绝连接。
- 通知对方:当前连接无效,不再进行数据传输。
适用场景:
- 服务器端口未监听,而客户端发起连接(服务器会返回 RST)。三次握手失败(发送端发送ACK,接收方没收到。导致在接收方没有认为建立连接时,发送方就发送数据)
- 非法数据包导致异常,主机决定终止连接。
- 通信一方崩溃或意外关闭,导致连接状态失效。
工作机制:
- 发送 RST 后,连接直接被关闭,未确认的数据也会丢失。
- 发送 RST 的一方不会进行四次挥手(正常 TCP 断开流程)。
- TCP 端口扫描工具(如 nmap)也会利用 RST 来检测端口状态。
3. URG 标志的作用
- URG 标志:指示当前 TCP 报文段中包含紧急数据。
- 紧急指针(Urgent Pointer):指明 紧急数据的结束位置(相对于当前序列号)。紧急指针的值 = 相对于序列号的偏移量,表示紧急数据的最后一个字节。
- 当前 TCP 序列号 = 1000
- 发送 10 个字节数据(包含 5 个紧急字节)
- 紧急指针 = 1005 (表示紧急数据到 1005 号字节 结束)
- 目的:让接收方的 TCP 优先处理这部分数据,而不是按正常 TCP 机制排队缓冲。
紧急数据一般位于报文数据的开头,假设紧急指针=2,当前报文序号为(SEQ)=1000,发送了10个字节,其中前3个是紧急数据,紧急数据的范围是1000~1002,紧急数据的最后一个字节的序列号是 1002。
虽然紧急数据可以在中间,但这样只能知道紧急数据的end位置,起始位置无法确定。但也可以通过约定紧急数据的长度来算出,紧急数据的起始位置。
eg.在 Telnet/SSH 这样的协议中,应用层约定紧急数据是单字节(比如
Ctrl+C
)。应用层可以检查紧急指针,并向前推测 1 个字节的位置。
超时重传机制
超时重传有两种情况,一种数据丢失没传到,另一种传到了,但确认信息没收到。
这两种情况发送端都不能确定有没有传到,剩余如果在特定时间间隔后,还没收到的话就会再传一次。
对于第二种,收到信息,确认应答丢包了。主机B会如何处理重新发过来的数据呢?
主机B维护一个 期望的序列号,用于检查数据包是否是新数据还是重复数据。如果收到的数据段的序列号 < 期望的序列号:说明这个数据段是重复的,主机B会 丢弃该数据段,但仍然会 重新发送 确认应答(ACK )确认它已经收到过该数据段。
TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控
制, 每次判定超时重发的超时时间都是 500ms 的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制(3次握手,4次挥手)
TCP一般要经过3次握手建立连接,4次挥手断开连接。
三次握手过程
阶段 客户端(Client) 服务器(Server) 第一次握手 发送 SYN 包,指定初始序列号 Seq = x
第二次握手 发送 SYN = 1, ACK = 1
,确认ACK = x+1
,同时发送Seq = y
第三次握手 发送 ACK = y+1
,确认服务器的SYN
连接建立,开始数据传输
四次挥手过程
阶段 客户端(Client) 服务器(Server) 第一次挥手 发送 FIN = 1, Seq = m
,表示不再发送数据第二次挥手 发送 ACK = m+1
,确认FIN
第三次挥手 发送 FIN = 1, Seq = n
,服务器关闭连接第四次挥手 发送 ACK = n+1
,确认服务器的FIN
连接完全关闭
为什么断开连接需要进行4次?
因为TCP是全双工的,客户端和服务端可以互相发信息。客户端给服务端断开连接,服务端也可以不断开连接 继续发消息,也可以选择断开连接。
为什么建立连接需要进行3次?
建立连接本质上也是4次,只不过确认和连接标志位同时为1,合并在一起。3次握手1.可以建立双方通信的共识。2.双方验证全双工信道的流畅性。
为什么断开连接的中间两次ACK+FIN不合并一块?可以,如果两方都需要断开。但一般情况下,当客户端断开连接时,服务端一般只会回确认,因为还有没发完的数据。等服务端发完数据才会断开。
分析4次挥手,主动关闭的一方和后关闭的一方所处的状态
我们把左边客户端当作主动关闭的一方,右边服务器作为后关闭的一方
TCP 连接断开时,通信双方都需要关闭各自的数据流通道,整个过程如下:
步骤 状态变化(客户端) 状态变化(服务器) 说明 ① 客户端发送 FIN ESTABLISHED
→FIN-WAIT-1
ESTABLISHED
→CLOSE-WAIT
客户端 发送 FIN
(Finish),表示 "我不再发送数据了"② 服务器回复 ACK FIN-WAIT-1
→FIN-WAIT-2
CLOSE-WAIT
服务器 回复 ACK
,表示 "我知道你要关闭发送数据的通道了"③ 服务器发送 FIN FIN-WAIT-2
CLOSE-WAIT
→LAST-ACK
服务器 可能还有数据要发送,等发送完毕后,再发送 FIN
关闭④ 客户端回复 ACK,进入 TIME-WAIT FIN-WAIT-2
→TIME-WAIT
LAST-ACK
→CLOSED
客户端 确认 FIN
后,发送ACK
,等待 2MSL 后真正关闭⑤ 连接完全关闭 TIME-WAIT
→CLOSED
CLOSED
连接彻底关闭
1.左边主动关闭,发送FIN,变为FIN-WAIT-1. (左边不再发送数据,但可接收)
右边接收到FIN,变为CLOSE-WAIT.(右边仍可以发送数据),发送ACK
2.左边接收ACK,变
FIN-WAIT-2.
(如果左边没收到
ACK
,左边会一直停留在FIN-WAIT-1
状态,直到超时或重传FIN
。)3.右边断开连接,发送FIN,变
LAST-ACK
(如果右边没有进行close(),断开连接会一直处于
CLOSE-WAIT,导致socket资源泄漏
)左边接收到FIN,
发送ACK,
变TIME-WAIT。等待
2MSL时长后,进入CLOSED状态。
(如果左边没收到FIN,会一直处于
FIN-WAIT-2,在Linux下等待60s 仍没有就会直接断开,发送ACK,变为TIME-WAIT
)4.右边收到ACK,变
CLOSED
(
LAST-ACK
状态下,右边若未收到ACK
,会重传FIN
,最终超时关闭。)
为什么主动关闭的一端(左边),
TIME-WAIT
→CLOSED 需要等待
2MSL时长。1.防止旧的重复数据包干扰。(等待历史的游离报文在网络中消失)
如果知道数据包会在网络中存在一定的延迟。我们双方关闭连接之后又重新建立了新连接,此时旧数据包才传到对方,就会被对方当成新的数据包处理。
MSL是指一个数据包从发送到从网络中消失的最大时间。2MSL是为了确保所有的数据包(包括FIN和ACK包)在网络中有足够的时间“消失”,避免任何晚到的包对新连接产生影响。
2.确保对方收到关闭连接的ACK (完成正常的4次挥手断开)
当我们发送ACK确认信息时,有可能会丢包,对方没收到就会重传FIN。这段时间就可以处理收到的FIN,并返回ACK。
如果被动关闭的一方没有调用close(),连接就不会发送回
FIN
包,导致连接永远停留在 CLOSE-WAIT 状态。这种情况会导致套接字资源(比如文件描述符)无法释放,从而造成资源泄漏,影响服务器的性能和稳定性。
主动方调用close(),发送FIN,状态ESTABLISHED → FIN-WAIT-1,在变为CLOSED前为什么还可以接收数据。close()不是关闭了读端写端了吗?
应用层
close()
:关闭的是应用对 socket 的访问权限,而非立即终止协议交互。调用close()后,应用层无法主动读取数据((因为
close()
已释放 fd)),但如果对方在收到 FIN 后仍有数据要发送,内核会接收这些数据并回复 ACK(确保对方知道数据已收到)。
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制。
当主机A接收到的报头中窗口大小为0,意味着主机B的缓冲区已经满了。主机A就不会再给B发报文,那什么时候能发呢?
主机A会定期发送一个窗口探测数据段(实际就是单个报头), 使接收端把窗口大小告诉发送端。
一开始怎么知道接收方缓冲区是否为满了?
3次握手,就相当于完成了获取窗口大小。
滑动窗口
之前我们说发一个报文,等ACK确认信息返回后,再发一个。这中模式比较慢,不如一次性发多个报文,效率更高。
但我们怎么知道这一次性发的报文,没有超过接收方缓冲区的容量呢?
这就需要TCP接收缓冲区滑动窗口的范围
发送方会有一个内部的缓冲区(通常称为 发送缓冲区)。在发送数据之前,发送方将数据放入这个缓冲区。
我们可以把发送缓冲区分为3个部分,1.已经发送完且确认完的2.可直接发送3.待发送
中间可以直接发送的部分就是滑动窗口的范围,怎么算出来的呢?
其实滑动窗口的大小就是对方发来报头中窗口大小,即对方缓冲区的剩余大小。(滑动窗口的大小不止和对方报头的窗口大小(剩余缓冲区大小)有关,还和拥塞窗口大小有关(即网络情况))滑动窗口开始位置start=确认序号(期待收到的起始字节)
结束位置end=start+窗口大小
所以在滑动窗口的数据可以直接发,不用担心会溢出接收方的缓冲区。
(为什么不发一个报文,包含滑动窗口的所以数据 ?数据链路层不允许发大的报文)
确认序号是一直增大的,所以滑动窗口一直向右边移动。滑动窗口一直向右移动,到了末尾会不会越界?不会,可以覆盖前面发送完且确认完的数据,把接收缓冲区理解为环形。
丢包
那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论.
一:ACK丢了
二:数据包丢了
不管哪种情况主机A的处理方法都是一样的。
比如上图,主机A发1~7000分7份发,其中1000~2000丢了。那主机B返回的所有报头中确认序号都为1001(期待下一个数据从1001开始),发送方在收到连续 3 个相同的 ACK 时,就会触发快重传,立即重传丢失的数据包,而不等超时重传。
如果中间丢了多个呢?1~1001 2001~3001都丢了,同样也是都返回1确认序号,补齐1~1001,再看其它是否缺少。
- 快重传机制会 按顺序重传每个丢失的数据段,并且每次快重传的触发都依赖于重复 ACK 的接收。
- 不会一次性重传所有丢失的段,即使中间丢失多个段,发送方会依次重传每一个丢失的段。
拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开
始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络
状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按
照多大的速度传输数据;
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速
度非常快.为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长,这个阶段就是拥塞避免。
阶段 | 发送数据的条件 | 增长方式 |
---|---|---|
慢启动 | 收到 ACK 后增加 cwnd 并发送更多数据 | 指数增长(×2) |
拥塞避免 | 到达ssthresh值后 增加 cwnd 并发送更多数据 | 线性增长(+1 MSS) |
拥塞发生 | 检测到丢包(超时或 3 个重复 ACK) | 进入慢启动或快速恢复,更新ssthres=拥塞窗口最大值/2 |
重新慢启动 | 从 1 MSS 开始 重新探测带宽 | 重新指数增长 |
因此我们发送数据大小时,不能只看接收方缓冲区的剩余大小还要看网络的传输情况,即
滑动窗口大小=min(接收方缓冲区剩余大小,拥塞窗口大小)
拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络
造成太大压力的折中方案.
延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;在 TCP 协议中,接收方在收到数据后不立即发送 ACK,而是稍作等待,希望能合并多个 ACK,从而减少 ACK 报文的数量,优化网络效率。这种机制称为 延迟应答。
TCP 延迟应答一般有两个作用:
1.合并 ACK(减少 ACK 数据包的数量)
延迟 ACK 机制 允许 TCP 等待一小段时间(通常 40~200ms),看看是否会收到第二个数据包:
- 如果在这个时间内收到了第二个数据包,TCP 就会合并 ACK,一次性确认多个数据包,减少 ACK 的数量。
- 如果没有收到第二个数据包,就会在超时时间到达时发送 ACK,确保数据不会被误认为丢失。
2. 让返回的窗口大小(rwnd)变大
延迟 ACK 让接收方有时间处理部分数据,并释放缓冲区,这样:
- 当接收方最终发送 ACK 时,可以报告一个更大的
rwnd
,让发送方继续发送更多数据。- 提高吞吐量,减少不必要的发送速率下降。
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
捎带应答
捎带应答(Piggyback Acknowledgment) 是 TCP 为了优化网络通信,在双向数据传输时,将 ACK(确认报文)捎带在返回的数据包里,而不是单独发送 ACK。
比如说三次握手:
接收方返回SYN+ACK也算捎带应答。
捎带应答的优缺点
优点 缺点 减少 ACK 包数量,降低网络开销 可能会增加延迟(如果 B 迟迟没有数据要发,ACK 可能会被延迟) 提高带宽利用率,减少不必要的小数据包 适用于双向数据传输,但对单向数据流(如文件下载)没有帮助 提升吞吐量,特别是在双向通信频繁的场景(如视频会议、VoIP) 需要应用层的配合,有些协议(如 Telnet)不适用
面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用 write 时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了,包合并成一个发送, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用 read 从接收缓冲区拿数据;
另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配, 例如:
写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节;
读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次;
粘包问题
粘包 是 TCP 面向字节流传输 的一个常见问题,它指的是 多个
send()
发送的数据被 TCP 合并到一个recv()
读取的缓冲区,导致接收方无法区分数据边界。1.数据包合并 2.发送速度太快导致合并 3.recv()不及时 都会导致接收方无法区分数据边界。
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界。
对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按 sizeof(Request)依次读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包
的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)
对于 UDP 协议来说, 是否也存在 "粘包问题" 呢?
UDP 不会发生 TCP 的“粘包”问题,因为 UDP 是“面向报文”的协议,每个
send()
发送的数据就是一个独立的数据报,不会被合并或拆分。UDP 是面向报文的,每个
sendto()
发送的数据在接收端recvfrom()
读取时仍然是完整的一个报文。
UDP 具有以下特点:
- 不会合并多个
sendto()
发送的数据。- 不会把一个
sendto()
发送的数据拆分给多个recvfrom()
读取。- 接收方
recvfrom()
一次最多读取一个 UDP 数据报,如果缓冲区小于数据报大小,超出部分会被直接丢弃(不会分成多个包)。- 站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报文, 要么不收. 不会出现"半个"的情况.
TCP 异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在,。一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset.
即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
TCP/UDP 对比
特性 | TCP(面向连接) | UDP(面向无连接) |
---|---|---|
连接模式 | 面向连接(需要 三次握手 建立连接) | 无连接(直接发送,不建立连接) |
可靠性 | 可靠,有超时重传、确认机制、流量控制、拥塞控制 | 不可靠,无重传机制,丢包不会被检测 |
数据传输方式 | 面向字节流,数据是连续的,无边界 | 面向报文,每个数据报是独立的 |
传输顺序 | 保证按序到达 | 不保证顺序,数据可能乱序 |
流量控制 | 有流量控制(滑动窗口) | 无流量控制 |
拥塞控制 | 有拥塞控制(慢启动、拥塞避免等) | 无拥塞控制 |
传输速度 | 较慢(需要握手、确认、重传) | 快(无握手、无确认机制) |
首部大小 | 20~60 字节 | 8 字节 |
是否支持广播/组播 | ❌ 不支持 | ✅ 支持广播、组播 |
适用场景 | 可靠传输(HTTP、文件传输、数据库、邮件) | 实时性要求高(视频、语音、DNS、游戏) |