计算机网络篇14:传输层——UDP 协议:8 字节定长报头划出报文边界,16 位端口号完成分用

📚 本文收录于「流浪」的系列专栏

🐧 Linux系统 ⚙️ C++
📊 数据结构与算法 🐍 Python
🔗 LangChain & LangGraph 🗄️ MySQL 数据库
🌿 Git 工具 🌐 计算机网络
🤖 LLM 💯 大厂面试、八股
📚 学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前言: 计算机网络篇8把 TCP 的报文边界交给了应用层自己切。UDP 反过来,用 8 个字节定长报头把边界在协议内部定死。本篇拆这 8 个字节:四个字段各管什么、定长怎么让报头与载荷天然分开、端口号怎么把载荷交到进程,再往下讲面向数据报、缓冲区,以及内核里一条报文是怎么被描述和处理的。


一、传输层把数据送到进程,五元组锁定一次通信

1.1 传输层交付的对象是进程而不是主机

1. 传输层负责端到端,逐跳转发不归它管
  1. 网络层及以下解决的是「数据怎么从一台主机挪到另一台主机」,沿途每一跳只关心下一站往哪走,不关心里面装的是什么
  2. 传输层解决的是「到了主机之后交给谁」,它的视野里只有两端:发送端主机上的某个进程、接收端主机上的某个进程
  3. 中间的路由器只实现到网络层为止,不碰传输层,所以传输层是真正意义上的端到端协议
2. UDP 与 TCP 在传输层给出不同的交付承诺
  1. UDP 承诺的是「尽力送到」:不建连接、不确认、不重传,报头只占 8 字节
  2. TCP 承诺的是「可靠送到」:建连接、编号、确认、重传、拥塞控制,报头 20 字节起步
  3. 两者的共同点比差异更基础------都靠端口号把数据交到进程,都有校验和,都要在 IP 之上再叠一层自己的头

IP 负责把数据送到哪台主机,传输层负责把它送进哪个进程。只有到了进程手里,一次网络传输才算真正完成。

1.2 五元组把一次通信从所有通信里挑出来

1. 四元组先锁定通信两端
  1. 源 IP 标识发送方主机,源端口标识发送方进程,目的 IP 和目的端口标识接收方进程,四个字段凑在一起,就能把这次通信从全网所有通信里单独挑出来
  2. 《计算机网络篇3》已经把四元组讲透------端口号为什么是 16 位、为什么不用 PID 标识网络进程,都在那一篇里
2. 协议号补上第五个元
  1. 同一台主机上可能同时跑着 TCP 服务和 UDP 服务,光看 IP 加端口只能定位到「这台主机的某个端口」,定位不到「来的这份数据该用哪套协议去拆」
  2. IP 报头里有一个协议号字段,它的取值决定这份有效载荷交给传输层的哪个协议处理:TCP 对应 6,UDP 对应 17
  3. 五个字段合起来才是完整的通信标识,netstat -n 打出来的那一列,就是这套组合在系统里的实际样子

四元组锁定两端,协议号锁定协议栈。少掉任何一个,同一台主机上并存的 TCP 和 UDP 就没法各走各的路。

1.3 端口号分段使用,知名端口固定给公开服务

1. 0 到 1023 留给公开服务
  1. 这一段叫知名端口号,专门给非常常用的应用层协议用,端口号固定下来,客户端不用问也知道该连哪个口
  2. 常见的几个:ssh 用 22,ftp 用 21,telnet 用 23,http 用 80,https 用 443
  3. 想看完整清单,直接 cat /etc/services,系统里所有服务名和端口号的对应关系都在里面
2. 1024 到 65535 交给操作系统动态分配
  1. 客户端程序一般不会自己去 bind 端口,由操作系统从这一段里挑一个空闲的分给它
  2. 所以同一台机器每次连服务器,源端口都可能不一样,这不是异常,是动态分配的正常表现
3. 自己写程序时要避开知名端口
  1. 把服务绑到 80 或 22 上,等于跟系统里已有的服务抢位置,bind 当场失败
  2. 选 1024 以上的号段,撞车的概率低得多
4. 一个进程可以绑多个端口,一个端口默认只归一个进程
  1. 一个进程可以创建多个 socket,每个绑不同的端口,对外看就是同一个程序开了好几个服务入口
  2. 反过来,同一个端口在同一时刻只能被一个进程占着,第二个进程去 bind 会失败并拿到 EADDRINUSE,这就是平时说的端口冲突
  3. 想突破后面这条限制得显式开口子:SO_REUSEADDR 解决的是处于 TIME_WAIT 的端口能否被重新绑定,SO_REUSEPORT 才是真正允许多个进程绑同一个端口做负载分担

端口号是给服务挂的固定门牌,门牌不变、租客可以换;想让两个租客共用一个门牌,得先显式声明复用,否则后来的只能被挡在门外。


二、UDP 报头是 8 字节定长结构,四个字段各管一件事

2.1 四字段各占 16 位,总共 8 字节

1. 两个 16 位端口号指定收发双方
  1. 最前面 4 个字节是源端口号和目的端口号,各占 16 位
  2. 源端口告诉对方「这条数据是谁发的、回包往哪送」,目的端口告诉本机「这条数据该交给谁」
  3. 源端口可以填 0,表示这次通信不需要对方回包
2. 16 位长度字段覆盖首部加数据
  1. 这个字段记的是整条数据报的长度,即 8 字节首部加上后面的数据,不是只记数据部分
  2. 最小值是 8,也就是只有头、没有数据;最大值是 65535
  3. 因为字段只有 16 位,单条 UDP 数据报的天花板在这里就被定死了
3. 16 位校验和出错就整条丢弃
  1. 发送方算一遍填进去,接收方拿到后重算一遍对比,对不上就把整条数据报扔掉
  2. 注意是「扔掉」而不是「要求重发」------UDP 没有重传机制,扔了就是扔了,应用层永远不会知道这条数据曾经存在过
  3. 算校验和时还要带上一个伪首部(源 IP、目的 IP、协议号、UDP 长度),这等于顺手验证了「这条数据确实送到了正确的主机、正确的协议手上」

四个 16 位字段凑够 8 字节,把收发双方、整条长度、数据完整性三件事装进同一个固定的壳里。

2.2 报头在内核里就是一个 8 字节结构体

1. 协议的实际形态是用结构体描述固定字段
  1. 报头格式一旦定下来,内核就不需要任何解析逻辑------它直接拿一个结构体去对应这 8 个字节
  2. Linux 里的定义大致长这样:
c 复制代码
struct udphdr {
    __be16 source;  /* 源端口号 */
    __be16 dest;    /* 目的端口号 */
    __be16 len;     /* UDP 长度:首部 + 数据 */
    __sum16 check;  /* 校验和 */
};
  1. 四个字段、每个 2 字节,结构体大小正好 8 字节,和报头的内存布局一一对应
2. 同为 C 语言实现,结构体可以直接挪到数据头部
  1. 封包时内核不需要逐字段拼字节数组,把一个 udphdr 填好,整体贴到数据前面就完事
  2. 之所以能这么干,是因为协议栈两端都是 C 语言写的,字段布局和对齐规则一致,不存在「这边排出来的结构体那边读不出来」的问题
  3. 更省事的是应用层压根感觉不到这些------你调 sendto 传一个 char 数组进去,内核在它前面长出一个 8 字节的头,你在应用层看不到任何痕迹
3. 端口字段上网络之前要过字节序转换
  1. 结构体直接贴省掉的是「字段布局」的麻烦,省不掉「字节序」的麻烦:主机字节序可能是小端,网络上规定用大端
  2. 所以端口号、长度这类多字节字段在填进报头之前必须过一遍 htons,收包时再过一遍 ntohs
  3. 大小端差异和这几个转换函数的细节在《计算机网络篇3》第六章,这里要记住的只有一条:报头的字节序由协议规定,不由主机决定

报头在报文里是一段字节,在内核里就是一个 8 字节结构体。布局由协议定死,实现就不用再做解析。

2.3 定长报头让分离不需要任何分隔符

1. 前 8 字节固定是报头,其后全是有效载荷
  1. 收包时照着头的大小读前 8 个字节,这 8 个字节怎么解释是固定的:前 2 个是源端口,再 2 个是目的端口,以此类推
  2. 读完这 8 个字节,指针往后一挪,剩下的从头到尾都是应用层数据,一个字节都不用猜
  3. 这就是「定长」带来的最大好处:分离规则不需要写进报文,因为它已经写死在协议里了
2. 封装反向同理,按 8 字节填头即可
  1. 发送方向下交付时,在数据前面预留 8 个字节,四个字段一填,封装就完成了
  2. 整个过程没有任何「长度前缀」「结束符」「转义字符」参与,报文能自我描述,全靠那个固定的 8 字节

定长报头把「哪里是头」这件事从报文内容里拎出来、写进了协议规定,分离因此不需要任何额外标记。


三、端口号完成分用,sendto 必须带目的端口

3.1 目的端口号决定报文最终交给哪个进程

1. 内核按目的端口查表找到对应 socket
  1. 报文到了传输层,内核从报头里取出目的端口号,拿这个值去查本机维护的映射表,找到挂在这个端口上的 socket
  2. 找到之后把数据塞进它的接收缓冲区,再唤醒等在这个 socket 上的进程
  3. 这张表怎么建、怎么查,《计算机网络篇3》第五章已经从头到尾走过一遍
2. sendto 必须带目的地址,因为报头里的目的端口只能由调用方给出
  1. 内核并不知道你要把数据发给谁------目标 IP 和目标端口是应用层才掌握的信息,只能通过 sendto 的参数递进来
  2. 内核拿到这两个值,才有东西往 IP 报头和 UDP 报头里填;没有它们,报头根本填不满,数据报发不出去
  3. TCP 里这件事被 connect 提前办掉了,所以后面的 send 不用再带地址;UDP 没有连接,每次都得现给,这也是 《计算机网络篇5》里 sendto 参数那一段的由来

目的端口号是唯一能把报文指到具体进程的字段,而它只能由调用方通过 sendto 递进来,内核自己变不出来。

3.2 分用分两级完成

1. 协议号先决定交给 TCP 还是 UDP
  1. IP 层收到报文,先看 IP 报头里的协议号:是 6 就交给 TCP,是 17 就交给 UDP
  2. 这一步发生在网络层往传输层交付的边界上,它决定了后面由谁来解析这段有效载荷
2. 端口号再决定交给哪个应用进程
  1. UDP 拿到报文,按定长规则读完前 8 个字节,从里面取出目的端口号
  2. 拿这个端口号去查 socket 表,找到对应进程,把剩下的有效载荷交上去

两级查表走完,数据从「某个 IP 上的一段字节」变成了「某个进程收到的一份数据」。

3. 端口号是 16 位,因为报头里这个字段就是 16 位
  1. 端口号的取值范围完全由报头字段的宽度决定,协议把端口字段定成 16 位,端口号就只能取 0 到 65535
  2. 这不是「内核恰好用了 16 位」,而是「协议规定端口字段 16 位,内核照着实现」
  3. 同一个道理,长度字段也是 16 位,所以单条数据报封顶 64K------字段宽度定死,能力边界跟着定死

分用是两次查表:协议号决定谁来拆这个包,目的端口决定拆完交给谁。UDP 只负责后一半,前一半在 IP 层就做完了。


四、面向数据报:UDP 不碰应用层交下来的报文边界

4.1 应用层交多长,UDP 就发多长

1. 既不拆分也不合并
  1. 应用层调一次 sendto 交过来 100 个字节,UDP 就封装成一条报文发出去,不会因为「太长了」切成两段,也不会因为「太短了」攒一攒再发
  2. 这个特性叫面向数据报,和 TCP 的面向字节流正好相反:TCP 把数据当成一串没有边界的字节,UDP 把每一次交付当成一条独立的报文
2. 一次 sendto 必须对应一次 recvfrom
  1. 用 UDP 传 100 个字节:发送端调一次 sendto 发出这 100 字节,接收端就必须调一次 recvfrom 把它整个读上来
  2. 不能循环调 10 次 recvfrom、每次读 10 个字节------第一次读走 10 个字节之后,这条数据报剩下的 90 个字节就被丢掉了
  3. 反向也一样:接收端一次 recvfrom 只取一条报文,哪怕缓冲区里排着三条,它也只会交第一条上来
3. 读写次数和数量无法灵活控制
  1. TCP 里可以随便写、随便读,写 3 次读 1 次也行,写 1 次读 5 次也行,字节流会把它们拼成一条连续数据
  2. UDP 做不到------发多少次就得收多少次,每次发多大就得每次收多大,两边的次数和长度必须严格对上
  3. 这就是面向数据报的代价:边界被保住了,灵活性被牺牲了

发几次就得收几次,发多大就得收多大。UDP 用「不灵活」换来了边界的完整。

4.2 报文边界来自 8 字节报头之后那段数据

1. 去掉定长报头,剩下的字节数就是这条报文的数据长度
  1. 一条 UDP 数据报拿到手,长度字段告诉你整条多长,减掉 8 就是数据部分多长
  2. 就算不看长度字段,靠定长规则也能算出来:读掉前 8 个字节,后面剩下的全是数据,从哪开始到哪结束毫无歧义
  3. 所以 UDP 的报文之间天然有边界,不需要任何分隔符去标
2. 每个数据报交给上层时长度是确定的
  1. recvfrom 返回时会告诉你这次读上来多少字节,这个数字就是这条数据报的完整长度
  2. 应用层拿到这个数字,就知道这条报文到哪结束、下一条从哪开始,不用自己去找边界
  3. 对比《计算机网络篇8》里 TCP 的处境:TCP 交给应用层的是字节流,边界得应用层照着自己的协议字段去切,切错了就是粘包
3. 边界的代价是长度上限和缓冲区要求
  1. 接收缓冲区必须能装下整条数据报,装不下就截断,截断的部分直接丢
  2. 单条数据报不能超过 64K,因为长度字段只有 16 位

UDP 的边界是算出来的,不是标出来的:定长报头一扣,剩下的长度就是这条报文的全部。

4.3 UDP 的三个特点都指向同一件事

1. 无连接:知道对端 IP 和端口就直接发
  1. 不需要三次握手,也不需要维护连接状态,sendto 的那一刻才第一次也是唯一一次用到对方的地址
  2. 好处是快------省掉了建连的往返时间,也省掉了维护连接状态的内核开销
2. 不可靠:没有确认与重传
  1. 发出去之后就不管了,对方收没收到 UDP 自己不知道,也不会去问
  2. 网络故障导致数据没送到,UDP 协议层不会给应用层返回任何错误信息,应用层以为发出去了,实际什么都没发生
  3. 校验和算出错的那条数据报同样直接丢弃,同样不通知
3. 面向数据报:读写次数和数量不由应用层灵活控制
  1. 发几次收几次、发多大收多大,两边对不上就会丢数据
  2. 所以每条报文多长、一次发几条,都得应用层自己规划好,协议不替你做这件事
4. 用寄信理解这三个特点
  1. 寄平信:地址写清楚投进邮筒就完事,不打电话确认对方在不在家------这是无连接
  2. 邮局不保证送达,丢了你也不知道,也没人会上门告诉你「这封信丢了」------这是不可靠
  3. 你寄一张照片就是一整张,对方收到也是一整张,邮局不会把照片剪成两半分两次送,也不会把两张照片粘成一张------这是面向数据报
  4. 三个特点其实是同一个设计选择的三面:UDP 只负责把整块数据甩出去,其余全交给应用层

UDP 的不可靠不是缺陷,是省下来的成本------它把「保证送到」整个推给了应用层,自己只保留把整块数据甩出去这一个动作。


五、缓冲区、64K 上限与应用层协议

5.1 没有真正意义上的发送缓冲区

1. sendto 直接交给内核,由内核继续往下推
  1. 调用 sendto,数据被拷进内核,内核当场封装成 UDP 数据报交给 IP 层,然后一路往下走
  2. sendto 一返回,就意味着数据已经交出去了,内核不会替你留一份等着重发
2. 无连接与不可靠决定了不需要留副本
  1. 留副本的唯一目的是重发,而重发需要两样东西:知道对方有没有收到、知道什么时候该重发
  2. UDP 两样都没有------既然永远不会重发,留副本就纯属浪费内存
  3. 所以「没有发送缓冲区」不是实现偷懒,是设计上根本不需要

不留副本的前提是永远不会重发;UDP 既没有确认也没有重传,发送方向自然不需要缓冲区。

5.2 接收缓冲区带来乱序与丢弃两件事

1. 缓冲区是报文排队等应用层来取的地方
  1. 数据报到达后先进接收缓冲区排队,应用层什么时候调 recvfrom,什么时候取走一条
  2. 有这块缓冲区,应用层就不必时时刻刻守着 socket,处理慢一点也不会立刻把数据丢掉
  3. 换个角度想:如果没有接收缓冲区,报文到达的那一刻应用层就必须已经在等,否则数据没地方放只能扔------缓冲区是把「网络到达的速度」和「应用处理的速度」解耦开的唯一办法
2. 不保证顺序,缓冲区满了就丢弃
  1. 缓冲区里的报文按到达顺序排队,而网络的到达顺序本来就不保证和发送顺序一致,后发的可能先到
  2. 缓冲区满之后,新到的数据报直接被丢弃,不通知任何人
3. 同一个 socket 既能读也能写
  1. 一个 UDP socket 上可以同时调 sendto 和 recvfrom,两个方向互不干扰
  2. 这个概念叫全双工,TCP 的 socket 同样如此

接收缓冲区是应用层处理速度和网络到达速度之间的垫子,代价是乱序和满则丢弃。

5.3 16 位长度字段把单个数据报锁在 64K 以内

1. 64K 包含 8 字节首部
  1. 长度字段最大 65535,指的是整条数据报(首部加数据)的长度
  2. 所以单条 UDP 能带的应用层数据是 65535 减 8,等于 65527 字节
  3. 放到 IPv4 环境里还要再扣 20 字节 IP 首部,实际能带 65507 字节
2. 超过 64K 只能在应用层自己分包
  1. 应用层把大数据切成若干块,每块自己加序号,分多次 sendto 发出去
  2. 接收端按序号收齐,自己拼回原来的数据
  3. UDP 不会帮你做这件事------分包、编号、拼装、缺块检测,全是应用层的活

64K 不是随手定的数,是 16 位字段能表示的最大值。字段宽度定在哪里,能力天花板就压在哪里。

5.4 基于 UDP 的应用层协议

1. 常见的几个
  1. NFS:网络文件系统
  2. TFTP:简单文件传输协议
  3. DHCP:动态主机配置协议
  4. BOOTP:启动协议,用于无盘设备启动
  5. DNS:域名解析协议
2. 它们选 UDP 的共同理由
  1. 一次请求一次响应,报文小,建连的开销比传输本身还大
  2. 丢了大不了重来一次,重试的成本远低于维护一条连接

用 UDP 写程序时自定义的那套报文格式,本身就是一份应用层协议,它和上面那几个有名有姓的协议在层次上没有任何区别。

会选 UDP 的协议都有同一个特征:报文小、一问一答、重试比等待便宜。


六、OS 怎么管住同机并存的成千上万个报文:先描述再组织

6.1 服务端单机内一定同时存在大量待处理报文

1. 应用层解析快慢不影响内核收报文
  1. 应用层还在解析上一条报文的时候,网卡可能又收到十几条了
  2. 内核不会停下来等应用层,报文照收,收下来的就堆在缓冲区里
  3. 所以任何一个服务端进程的 socket 缓冲区里、内核协议栈里,任何时刻都可能同时躺着大量报文
2. 管理方式先描述再组织
  1. 操作系统管理任何一批对象都是同一个套路:先用一个结构体把每个对象描述清楚,再用某种数据结构把它们组织起来
  2. 报文也一样------每条报文先有一个结构体描述它(数据在哪、多长、属于哪个 socket),再用链表和队列串起来

报文的到达节奏由网络决定、不由应用层决定,所以内核必须先把它们收下来,再慢慢处理。

6.2 struct sk_buff 用四个指针描述一块报文缓冲区

cpp 复制代码
struct sk_buff {
    // 双向链表,skb可以串成队列
    struct sk_buff *next;
    struct sk_buff *prev;

    // 四个核心指针,描述一块报文缓冲区
    unsigned char *head;  // 整块buffer内存起始,固定
    unsigned char *data;  // 当前有效数据起始,可变
    unsigned char *tail;  // 当前有效数据末尾,可变
    unsigned char *end;   // 整块buffer内存末尾,固定

    unsigned int len;     // 当前有效数据长度: tail - data
    unsigned int truesize;// sk_buff结构体 + 数据buffer总大小

    // 协议头偏移(不是指针,是偏移量)
    __u16 mac_header;     // mac头偏移
    __u16 network_header; // 网络层头偏移
    __u16 transport_header;//传输层头偏移

    struct net_device *dev; // 绑定的网卡设备
};
1. head 与 data 划出预留区和数据起点
  1. head 指向这块内存的起始地址
  2. data 指向当前有效数据的起点------注意是「当前」,因为它会移动
  3. head 和 data 之间的空隙是留给各层协议头用的,封装时新头就长在这段空隙里
2. tail 与 end 划出数据末尾和内存末尾
  1. tail 指向当前数据的末尾,end 指向整块内存的末尾
  2. 数据区在 data 到 tail 之间,整块内存区在 head 到 end 之间
3. 一条报文的内存由报头与有效载荷两部分构成
  1. 应用层刚交下来时,这块内存里只有有效载荷
  2. 每向下走一层,就有一个新的报头被填进 data 前面那段预留区

四个指针把一块内存切成三段:预留区留给报头,数据区装有效载荷,尾部空间留着扩容。

6.3 封包与解包本质是移动 data 指针

1. 每向下交付一层,data 前移对应报头大小
  1. UDP 往下交给 IP 层时,在 data 前面填 20 字节 IP 报头,然后把 data 往前挪 20 字节,让它指向新的有效数据起点
  2. 再往下交给链路层,填一个以太网头,data 继续前移
  3. 走到最底下,head 到 data 之间堆满了各层报头,data 到 tail 之间还是应用层那份原始数据
2. 解包时 data 后移,正好相反
  1. 收到报文,链路层先读自己的头,读完把 data 往后挪,交给网络层
  2. IP 层读 IP 头,读完再挪,交给传输层
  3. UDP 拿到之后读前 8 个字节的报头,data 再往后挪 8 字节,剩下的就是应用层数据
3. 接收方的报文以 sk_buff 形态被双链表与队列管理
  1. 内核收到的是一条 sk_buff,不是一段裸字节
  2. 每个 socket 的接收缓冲区就是一个 sk_buff 队列,报文到达就入队,recvfrom 就出队
4. UDP 报头与载荷的分离、分用由此统一解释
  1. 分离:data 指针跳过 8 字节,报头和载荷就分开了------「分离」在实现层面就是挪指针
  2. 分用:从这 8 个字节里读出目的端口号,查表找到 socket,把 data 指向的那段载荷塞进它的队列
5. 真实内核的 sk_buff 比四指针模型复杂
  1. 真正的 struct sk_buff 字段远不止四个指针,还带着协议头偏移量、校验和状态、分片信息、路由缓存一大堆成员
  2. 四指针模型是把「封包解包靠移动指针」这件事抽出来的最小模型,理解机制用它够了,去看真实源码时不要指望只有这四个字段

封包不是把数据一层层拷进新缓冲区,而是让 data 指针在预留空间里向前走;解包就是把指针往后挪回来。


七、全篇总结

1. 传输层的职责是交付到进程
  1. 传输层负责端到端,交付对象是进程而不是主机
  2. 五元组(源 IP、源端口、目的 IP、目的端口、协议号)唯一标识一次通信
2. UDP 报头是 8 字节定长结构
  1. 源端口、目的端口、长度、校验和各占 16 位
  2. 定长报头让分离不需要分隔符,读掉 8 字节剩下的就是有效载荷
3. 分用靠两级查表
  1. 协议号决定交给 TCP 还是 UDP
  2. 目的端口决定交给哪个进程,sendto 因此必须带目的地址
4. 面向数据报保住了报文边界
  1. 应用层交多长就发多长,不拆分也不合并
  2. 一次 sendto 对应一次 recvfrom,边界来自定长报头之后那段数据
5. 发送方向不缓存、接收方向要排队
  1. 没有发送缓冲区,因为不重发就不需要留副本
  2. 有接收缓冲区,代价是乱序和满则丢弃
6. 内核用 sk_buff 描述并组织报文
  1. 先描述再组织,四指针是 head、data、tail、end
  2. 封包解包本质是移动 data 指针

八、文末面试题

8.1 推导题(按本篇知识点,附答案)

  1. 【推导】UDP 报头里没有「首部长度」字段,接收方怎么知道前 8 个字节是头、后面全是数据?

    答:因为报头长度由协议规定死是 8 字节,接收方读满 8 个字节就直接切,不需要报文自己声明头有多长;IP 报头必须有首部长度字段,是因为它的长度可变(带选项时不固定)。

  2. 【推导】UDP 报头里既没有「首部长度」也没有「序号」,而 TCP 两个都有,为什么?

    答:首部长度的差别来自可变性------UDP 首部固定 8 字节不用声明,TCP 首部带选项、长度可变必须自带字段说明;序号的差别来自职责------TCP 靠序号做按序交付和重传判定,UDP 这两件事都不做,留个序号字段也没有任何用途。

  3. 【推导】sendto 为什么每次都要带目的地址,而 TCP 的 send 不用?

    答:UDP 没有连接,报头里的目的 IP 和目的端口只能由本次调用传进来,内核没有别的地方可查;TCP 在建连时已经把对端地址记在连接控制块里,后续 send 直接取用。

  4. 【推导】UDP 为什么没有发送缓冲区,却必须有接收缓冲区?

    答:发送方向不需要缓冲区,是因为不可靠协议不会重发,留副本没有任何用途;接收方向必须有缓冲区,是因为报文到达的时刻由网络决定、应用层取数据的时刻由自己决定,中间需要一块地方先把报文接住,否则报文一到就只能丢弃。

  5. 【推导】sk_buff 的 data 指针既能表示封包又能表示解包,同一套机制为什么能同时干两件事?

    答:因为封包和解包都是「在预留空间里改变有效数据的起点」------封包把 data 向前移、让新报头落在 data 前面,解包把 data 向后移、把已经读过的报头排除在外,两者对 data 的操作方向相反但语义完全一致。

  6. 【推导】UDP 的接收缓冲区满了以后,新到的报文为什么直接丢,而不是像 TCP 那样让发送方慢下来?

    答:因为 UDP 没有流量控制,接收方没有任何手段把「我慢了」这个消息传回发送方,两端之间不存在窗口这回事;缓冲区是这个方向上唯一的缓冲垫,垫满了就只能丢弃,且丢弃后不通知任何人。

8.2 真题(来源已核实,转述注明)

  1. UDP 报头有哪些字段?报头和有效载荷是怎么分离、又怎么交付给上层的?

    答:固定 8 字节、四个字段各 16 位,依次是源端口号、目的端口号、UDP 长度(整条数据报的长度,含 8 字节首部)、UDP 校验和;分离靠定长,读满前 8 个字节即为报头、其后全是有效载荷;交付靠分用,从报头取出目的端口号,查 socket 映射表找到对应进程再把有效载荷交上去。

    【真题·转述自 牛客网《后端面试高频:UDP协议》】

  2. UDP 的校验和是怎么算的?算出来对不上会怎么处理?

    答:发送方把伪首部(源 IP、目的 IP、协议号、UDP 长度)、UDP 首部和数据一起按 16 位分组做反码求和,结果取反填入校验和字段;接收方用同样的方法重算并比对,不一致就把整条数据报丢弃,而且不会通知应用层。

    【真题·转述自 CSDN《UDP协议面试指南》】

  3. 怎么理解 UDP 的面向数据报?

    答:应用层交给 UDP 多长的报文,UDP 就原样发送,既不拆分也不合并;发送端一次 sendto 发 100 字节,接收端就必须一次 recvfrom 收 100 字节,不能循环 10 次每次收 10 字节,读写次数和数量都无法灵活控制。

    【真题·转述自 牛客网《后端面试高频:UDP协议》】

  4. UDP 会粘包吗?为什么?

    答:不会。粘包的前提是协议把数据当成无边界字节流,TCP 正是如此,多次 write 可能被一次 read 读完;UDP 面向数据报,一次 sendto 的数据在接收端正好由一次 recvfrom 完整取出,边界由协议保留,既不会合并也不会拆分。

    【真题·转述自 CSDN《UDP协议面试指南》】

  5. 一条 UDP 数据报最大能传多少数据?业务数据超过这个长度怎么办?

    答:报头长度字段 16 位,单条数据报上限 65535 字节,扣掉 8 字节首部后数据区最多 65527 字节,在 IPv4 下再扣 20 字节 IP 首部剩 65507 字节;超过就得在应用层手动分包,每块加序号分多次发送,接收端按序号手动拼装。

    【真题·转述自 CSDN《UDP协议面试指南》】

  6. DNS 为什么主要用 UDP 而不是 TCP?

    答:DNS 查询是一问一答、报文很小,UDP 省掉建连的往返,一次查询一个来回就能出结果,客户端重试的成本也远低于维持一条 TCP 连接;只有在响应超过 512 字节或做区域传送时才会改用 TCP。

    【真题·转述自 CSDN《UDP协议面试指南》】


UDP 把能省的全省了,只留下一个 8 字节的报头和一句「送到就到、不到就算」。它不保证的那些事,最后都得由应用层自己补上。

相关推荐
MicrosoftCloud4 小时前
计网 09|TCP 流量控制与拥塞控制:rwnd 滑动窗口 + 慢开始/拥塞避免/快重传快恢复
计算机网络·考研·tcp·流量控制·拥塞避免·拥塞控制·慢开始
CQU_JIAKE5 小时前
10.29计算机网络
计算机网络
流浪0019 小时前
计算机网络篇13:应用层——HTTP 不记录用户状态,Cookie 在客户端存凭证、Session 在服务端存数据
计算机网络·cookie·session·http协议
从琳开始98912 小时前
从Socket到UDP回显服务器:Java网络编程完整指南
java·网络·网络协议·udp·java-ee
Lhan.zzZ12 小时前
用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程
c++·vscode·qt·tcp/ip·udp
Dwzun13 小时前
基于SpringBoot3+Vue3的油莎豆种植管理系统
java·前端·vue.js·spring boot·后端·python·计算机网络
听风说ml1 天前
socket编程 【C语言】
c语言·udp·tcp·socket编程
lazy H1 天前
【计算机网络】第 13 篇:NAT 和端口映射是什么?为什么本地服务外网访问不了?
java·网络·网络协议·学习·计算机网络·408
zl_dfq1 天前
Windows 下 UDP 套接字编程指南
网络·windows·udp