UDP协议 TCP协议(格式 超时重传 滑动窗口 拥塞控制...)

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协议

格式

  1. 源端口号(16位):发送端应用程序的端口号。
  2. 目标端口号(16位):接收端应用程序的端口号。
  3. 序列号(32位):表示发送方发送的数据的顺序号,确保数据可以按顺序接收。
  4. 确认号(32位):如果ACK标志位为1,则表示接收到的数据包的下一个期望字节的序列号。
  5. 4位首部长度(数据偏移)(4位):指示TCP报文段的头部长度,单位是32bit(4字节)。
  6. 保留(3位):用于未来的扩展,目前值为0。
  7. 标志位(9位):标志位用于控制TCP的行为。9个标志位分别为:
  8. 标志位缩写作用
    紧急(Urgent)URG表示该数据是紧急数据,需优先处理
    确认(Acknowledgment)ACK该数据包携带有效的 ACK 号
    推送(Push)PSH立即交付数据给应用层
    复位(Reset)RST立即重置连接
    同步(Synchronization)SYN用于建立连接(TCP 三次握手)
    终止(Finish)FIN终止连接(TCP 四次挥手)
    回显(ECE)ECE显式拥塞通知
    拥塞窗口减少(CWR)CWR发送方已减少拥塞窗口
    非拥塞回显(NS)NSECN 扩展
  9. 窗口大小(16位):接收方的接收窗口大小,用于流量控制。
  10. 校验和(16位):用于保证数据在传输过程中没有被篡改。
  11. 紧急指针(16位):如果URG标志位为1,则该字段有效,指示紧急数据的偏移。
  12. 选项(可选,长度可变):可以包含一些附加的TCP选项,如最大段大小(MSS)等。
  13. 数据(可变长度):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次挥手断开连接。

三次握手过程

ACK(确认序号) 和 Seq(序号)SYN(建立连接)

阶段客户端(Client)服务器(Server)
第一次握手发送 SYN=1 包,指定初始序列号 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 连接断开时,通信双方都需要关闭各自的数据流通道,整个过程如下:

步骤状态变化(客户端)状态变化(服务器)说明
① 客户端发送 FINESTABLISHEDFIN-WAIT-1ESTABLISHEDCLOSE-WAIT客户端 发送 FIN(Finish),表示 "我不再发送数据了"
② 服务器回复 ACKFIN-WAIT-1FIN-WAIT-2CLOSE-WAIT服务器 回复 ACK,表示 "我知道你要关闭发送数据的通道了"
③ 服务器发送 FINFIN-WAIT-2CLOSE-WAITLAST-ACK服务器 可能还有数据要发送,等发送完毕后,再发送 FIN 关闭
④ 客户端回复 ACK,进入 TIME-WAITFIN-WAIT-2TIME-WAITLAST-ACKCLOSED客户端 确认 FIN 后,发送 ACK,等待 2MSL 后真正关闭
⑤ 连接完全关闭TIME-WAITCLOSEDCLOSED连接彻底关闭

 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-WAITCLOSED 需要等待2MSL时长

1.防止旧的重复数据包干扰。(等待历史的游离报文在网络中消失)

如果知道数据包会在网络中存在一定的延迟。我们双方关闭连接之后又重新建立了新连接,此时旧数据包才传到对方,就会被对方当成新的数据包处理。

MSL是指一个数据包从发送到从网络中消失的最大时间。2MSL是为了确保所有的数据包(包括FIN和ACK包)在网络中有足够的时间“消失”,避免任何晚到的包对新连接产生影响。

2.确保对方收到关闭连接的ACK  (完成正常的4次挥手断开)

当我们发送ACK确认信息时,有可能会丢包,对方没收到就会重传FIN。这段时间就可以处理收到的FIN,并返回ACK。

为什么等待2MSL?

1.第一个MSL用来等待对方没收到ACK 超时重传FIN的情况

2.第二个MSL等待自己发送的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也算捎带应答。

捎带应答的优缺点

优点有三个:

  1. 减少报文数量,降低网络开销

  2. 降低协议头部比例,提高带宽利用率

  3. 减轻接收方处理负担,减少中断

缺点有两个:

  1. ACK 可能因为等待数据捎带而延迟,影响对方窗口滑动或触发重传

  2. 实现上需要与 Delayed ACK 策略配合处理,逻辑复杂、调优难度更高

面向字节流

创建一个 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、游戏)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值