
📚 本文收录于「流浪」的系列专栏
| 🐧 Linux系统 | ⚙️ C++ |
| 📊 数据结构与算法 | 🐍 Python |
| 🔗 LangChain & LangGraph | 🗄️ MySQL 数据库 |
| 🌿 Git 工具 | 🌐 计算机网络 |
| 🤖 LLM | 💯 大厂面试、八股 |
| 📚 学习筑基专栏 |
🏠 博客主页:流浪 | 📝 原创首发于 CSDN
前言: 计算机网络篇8把 TCP 的报文边界交给了应用层自己切。UDP 反过来,用 8 个字节定长报头把边界在协议内部定死。本篇拆这 8 个字节:四个字段各管什么、定长怎么让报头与载荷天然分开、端口号怎么把载荷交到进程,再往下讲面向数据报、缓冲区,以及内核里一条报文是怎么被描述和处理的。
一、传输层把数据送到进程,五元组锁定一次通信
1.1 传输层交付的对象是进程而不是主机
1. 传输层负责端到端,逐跳转发不归它管
- 网络层及以下解决的是「数据怎么从一台主机挪到另一台主机」,沿途每一跳只关心下一站往哪走,不关心里面装的是什么
- 传输层解决的是「到了主机之后交给谁」,它的视野里只有两端:发送端主机上的某个进程、接收端主机上的某个进程
- 中间的路由器只实现到网络层为止,不碰传输层,所以传输层是真正意义上的端到端协议
2. UDP 与 TCP 在传输层给出不同的交付承诺
- UDP 承诺的是「尽力送到」:不建连接、不确认、不重传,报头只占 8 字节
- TCP 承诺的是「可靠送到」:建连接、编号、确认、重传、拥塞控制,报头 20 字节起步
- 两者的共同点比差异更基础------都靠端口号把数据交到进程,都有校验和,都要在 IP 之上再叠一层自己的头
IP 负责把数据送到哪台主机,传输层负责把它送进哪个进程。只有到了进程手里,一次网络传输才算真正完成。
1.2 五元组把一次通信从所有通信里挑出来
1. 四元组先锁定通信两端
- 源 IP 标识发送方主机,源端口标识发送方进程,目的 IP 和目的端口标识接收方进程,四个字段凑在一起,就能把这次通信从全网所有通信里单独挑出来
- 《计算机网络篇3》已经把四元组讲透------端口号为什么是 16 位、为什么不用 PID 标识网络进程,都在那一篇里
2. 协议号补上第五个元

- 同一台主机上可能同时跑着 TCP 服务和 UDP 服务,光看 IP 加端口只能定位到「这台主机的某个端口」,定位不到「来的这份数据该用哪套协议去拆」
- IP 报头里有一个协议号字段,它的取值决定这份有效载荷交给传输层的哪个协议处理:TCP 对应 6,UDP 对应 17
- 五个字段合起来才是完整的通信标识,
netstat -n打出来的那一列,就是这套组合在系统里的实际样子
四元组锁定两端,协议号锁定协议栈。少掉任何一个,同一台主机上并存的 TCP 和 UDP 就没法各走各的路。
1.3 端口号分段使用,知名端口固定给公开服务
1. 0 到 1023 留给公开服务
- 这一段叫知名端口号,专门给非常常用的应用层协议用,端口号固定下来,客户端不用问也知道该连哪个口
- 常见的几个:ssh 用 22,ftp 用 21,telnet 用 23,http 用 80,https 用 443
- 想看完整清单,直接
cat /etc/services,系统里所有服务名和端口号的对应关系都在里面

2. 1024 到 65535 交给操作系统动态分配
- 客户端程序一般不会自己去 bind 端口,由操作系统从这一段里挑一个空闲的分给它
- 所以同一台机器每次连服务器,源端口都可能不一样,这不是异常,是动态分配的正常表现
3. 自己写程序时要避开知名端口
- 把服务绑到 80 或 22 上,等于跟系统里已有的服务抢位置,bind 当场失败
- 选 1024 以上的号段,撞车的概率低得多
4. 一个进程可以绑多个端口,一个端口默认只归一个进程
- 一个进程可以创建多个 socket,每个绑不同的端口,对外看就是同一个程序开了好几个服务入口
- 反过来,同一个端口在同一时刻只能被一个进程占着,第二个进程去 bind 会失败并拿到 EADDRINUSE,这就是平时说的端口冲突
- 想突破后面这条限制得显式开口子:
SO_REUSEADDR解决的是处于 TIME_WAIT 的端口能否被重新绑定,SO_REUSEPORT才是真正允许多个进程绑同一个端口做负载分担
端口号是给服务挂的固定门牌,门牌不变、租客可以换;想让两个租客共用一个门牌,得先显式声明复用,否则后来的只能被挡在门外。
二、UDP 报头是 8 字节定长结构,四个字段各管一件事

2.1 四字段各占 16 位,总共 8 字节
1. 两个 16 位端口号指定收发双方
- 最前面 4 个字节是源端口号和目的端口号,各占 16 位
- 源端口告诉对方「这条数据是谁发的、回包往哪送」,目的端口告诉本机「这条数据该交给谁」
- 源端口可以填 0,表示这次通信不需要对方回包
2. 16 位长度字段覆盖首部加数据
- 这个字段记的是整条数据报的长度,即 8 字节首部加上后面的数据,不是只记数据部分
- 最小值是 8,也就是只有头、没有数据;最大值是 65535
- 因为字段只有 16 位,单条 UDP 数据报的天花板在这里就被定死了
3. 16 位校验和出错就整条丢弃
- 发送方算一遍填进去,接收方拿到后重算一遍对比,对不上就把整条数据报扔掉
- 注意是「扔掉」而不是「要求重发」------UDP 没有重传机制,扔了就是扔了,应用层永远不会知道这条数据曾经存在过
- 算校验和时还要带上一个伪首部(源 IP、目的 IP、协议号、UDP 长度),这等于顺手验证了「这条数据确实送到了正确的主机、正确的协议手上」
四个 16 位字段凑够 8 字节,把收发双方、整条长度、数据完整性三件事装进同一个固定的壳里。
2.2 报头在内核里就是一个 8 字节结构体
1. 协议的实际形态是用结构体描述固定字段
- 报头格式一旦定下来,内核就不需要任何解析逻辑------它直接拿一个结构体去对应这 8 个字节
- Linux 里的定义大致长这样:
c
struct udphdr {
__be16 source; /* 源端口号 */
__be16 dest; /* 目的端口号 */
__be16 len; /* UDP 长度:首部 + 数据 */
__sum16 check; /* 校验和 */
};
- 四个字段、每个 2 字节,结构体大小正好 8 字节,和报头的内存布局一一对应
2. 同为 C 语言实现,结构体可以直接挪到数据头部
- 封包时内核不需要逐字段拼字节数组,把一个 udphdr 填好,整体贴到数据前面就完事
- 之所以能这么干,是因为协议栈两端都是 C 语言写的,字段布局和对齐规则一致,不存在「这边排出来的结构体那边读不出来」的问题
- 更省事的是应用层压根感觉不到这些------你调 sendto 传一个 char 数组进去,内核在它前面长出一个 8 字节的头,你在应用层看不到任何痕迹
3. 端口字段上网络之前要过字节序转换
- 结构体直接贴省掉的是「字段布局」的麻烦,省不掉「字节序」的麻烦:主机字节序可能是小端,网络上规定用大端
- 所以端口号、长度这类多字节字段在填进报头之前必须过一遍
htons,收包时再过一遍ntohs - 大小端差异和这几个转换函数的细节在《计算机网络篇3》第六章,这里要记住的只有一条:报头的字节序由协议规定,不由主机决定
报头在报文里是一段字节,在内核里就是一个 8 字节结构体。布局由协议定死,实现就不用再做解析。
2.3 定长报头让分离不需要任何分隔符
1. 前 8 字节固定是报头,其后全是有效载荷
- 收包时照着头的大小读前 8 个字节,这 8 个字节怎么解释是固定的:前 2 个是源端口,再 2 个是目的端口,以此类推
- 读完这 8 个字节,指针往后一挪,剩下的从头到尾都是应用层数据,一个字节都不用猜
- 这就是「定长」带来的最大好处:分离规则不需要写进报文,因为它已经写死在协议里了
2. 封装反向同理,按 8 字节填头即可
- 发送方向下交付时,在数据前面预留 8 个字节,四个字段一填,封装就完成了
- 整个过程没有任何「长度前缀」「结束符」「转义字符」参与,报文能自我描述,全靠那个固定的 8 字节
定长报头把「哪里是头」这件事从报文内容里拎出来、写进了协议规定,分离因此不需要任何额外标记。
三、端口号完成分用,sendto 必须带目的端口
3.1 目的端口号决定报文最终交给哪个进程

1. 内核按目的端口查表找到对应 socket
- 报文到了传输层,内核从报头里取出目的端口号,拿这个值去查本机维护的映射表,找到挂在这个端口上的 socket
- 找到之后把数据塞进它的接收缓冲区,再唤醒等在这个 socket 上的进程
- 这张表怎么建、怎么查,《计算机网络篇3》第五章已经从头到尾走过一遍
2. sendto 必须带目的地址,因为报头里的目的端口只能由调用方给出
- 内核并不知道你要把数据发给谁------目标 IP 和目标端口是应用层才掌握的信息,只能通过 sendto 的参数递进来
- 内核拿到这两个值,才有东西往 IP 报头和 UDP 报头里填;没有它们,报头根本填不满,数据报发不出去
- TCP 里这件事被 connect 提前办掉了,所以后面的 send 不用再带地址;UDP 没有连接,每次都得现给,这也是 《计算机网络篇5》里 sendto 参数那一段的由来
目的端口号是唯一能把报文指到具体进程的字段,而它只能由调用方通过 sendto 递进来,内核自己变不出来。
3.2 分用分两级完成
1. 协议号先决定交给 TCP 还是 UDP
- IP 层收到报文,先看 IP 报头里的协议号:是 6 就交给 TCP,是 17 就交给 UDP
- 这一步发生在网络层往传输层交付的边界上,它决定了后面由谁来解析这段有效载荷
2. 端口号再决定交给哪个应用进程
- UDP 拿到报文,按定长规则读完前 8 个字节,从里面取出目的端口号
- 拿这个端口号去查 socket 表,找到对应进程,把剩下的有效载荷交上去
两级查表走完,数据从「某个 IP 上的一段字节」变成了「某个进程收到的一份数据」。
3. 端口号是 16 位,因为报头里这个字段就是 16 位
- 端口号的取值范围完全由报头字段的宽度决定,协议把端口字段定成 16 位,端口号就只能取 0 到 65535
- 这不是「内核恰好用了 16 位」,而是「协议规定端口字段 16 位,内核照着实现」
- 同一个道理,长度字段也是 16 位,所以单条数据报封顶 64K------字段宽度定死,能力边界跟着定死
分用是两次查表:协议号决定谁来拆这个包,目的端口决定拆完交给谁。UDP 只负责后一半,前一半在 IP 层就做完了。
四、面向数据报:UDP 不碰应用层交下来的报文边界
4.1 应用层交多长,UDP 就发多长
1. 既不拆分也不合并
- 应用层调一次 sendto 交过来 100 个字节,UDP 就封装成一条报文发出去,不会因为「太长了」切成两段,也不会因为「太短了」攒一攒再发
- 这个特性叫面向数据报,和 TCP 的面向字节流正好相反:TCP 把数据当成一串没有边界的字节,UDP 把每一次交付当成一条独立的报文
2. 一次 sendto 必须对应一次 recvfrom
- 用 UDP 传 100 个字节:发送端调一次 sendto 发出这 100 字节,接收端就必须调一次 recvfrom 把它整个读上来
- 不能循环调 10 次 recvfrom、每次读 10 个字节------第一次读走 10 个字节之后,这条数据报剩下的 90 个字节就被丢掉了
- 反向也一样:接收端一次 recvfrom 只取一条报文,哪怕缓冲区里排着三条,它也只会交第一条上来
3. 读写次数和数量无法灵活控制
- TCP 里可以随便写、随便读,写 3 次读 1 次也行,写 1 次读 5 次也行,字节流会把它们拼成一条连续数据
- UDP 做不到------发多少次就得收多少次,每次发多大就得每次收多大,两边的次数和长度必须严格对上
- 这就是面向数据报的代价:边界被保住了,灵活性被牺牲了
发几次就得收几次,发多大就得收多大。UDP 用「不灵活」换来了边界的完整。
4.2 报文边界来自 8 字节报头之后那段数据
1. 去掉定长报头,剩下的字节数就是这条报文的数据长度
- 一条 UDP 数据报拿到手,长度字段告诉你整条多长,减掉 8 就是数据部分多长
- 就算不看长度字段,靠定长规则也能算出来:读掉前 8 个字节,后面剩下的全是数据,从哪开始到哪结束毫无歧义
- 所以 UDP 的报文之间天然有边界,不需要任何分隔符去标
2. 每个数据报交给上层时长度是确定的
- recvfrom 返回时会告诉你这次读上来多少字节,这个数字就是这条数据报的完整长度
- 应用层拿到这个数字,就知道这条报文到哪结束、下一条从哪开始,不用自己去找边界
- 对比《计算机网络篇8》里 TCP 的处境:TCP 交给应用层的是字节流,边界得应用层照着自己的协议字段去切,切错了就是粘包
3. 边界的代价是长度上限和缓冲区要求
- 接收缓冲区必须能装下整条数据报,装不下就截断,截断的部分直接丢
- 单条数据报不能超过 64K,因为长度字段只有 16 位
UDP 的边界是算出来的,不是标出来的:定长报头一扣,剩下的长度就是这条报文的全部。
4.3 UDP 的三个特点都指向同一件事
1. 无连接:知道对端 IP 和端口就直接发
- 不需要三次握手,也不需要维护连接状态,sendto 的那一刻才第一次也是唯一一次用到对方的地址
- 好处是快------省掉了建连的往返时间,也省掉了维护连接状态的内核开销
2. 不可靠:没有确认与重传
- 发出去之后就不管了,对方收没收到 UDP 自己不知道,也不会去问
- 网络故障导致数据没送到,UDP 协议层不会给应用层返回任何错误信息,应用层以为发出去了,实际什么都没发生
- 校验和算出错的那条数据报同样直接丢弃,同样不通知
3. 面向数据报:读写次数和数量不由应用层灵活控制
- 发几次收几次、发多大收多大,两边对不上就会丢数据
- 所以每条报文多长、一次发几条,都得应用层自己规划好,协议不替你做这件事
4. 用寄信理解这三个特点
- 寄平信:地址写清楚投进邮筒就完事,不打电话确认对方在不在家------这是无连接
- 邮局不保证送达,丢了你也不知道,也没人会上门告诉你「这封信丢了」------这是不可靠
- 你寄一张照片就是一整张,对方收到也是一整张,邮局不会把照片剪成两半分两次送,也不会把两张照片粘成一张------这是面向数据报
- 三个特点其实是同一个设计选择的三面:UDP 只负责把整块数据甩出去,其余全交给应用层
UDP 的不可靠不是缺陷,是省下来的成本------它把「保证送到」整个推给了应用层,自己只保留把整块数据甩出去这一个动作。
五、缓冲区、64K 上限与应用层协议
5.1 没有真正意义上的发送缓冲区
1. sendto 直接交给内核,由内核继续往下推
- 调用 sendto,数据被拷进内核,内核当场封装成 UDP 数据报交给 IP 层,然后一路往下走
- sendto 一返回,就意味着数据已经交出去了,内核不会替你留一份等着重发
2. 无连接与不可靠决定了不需要留副本
- 留副本的唯一目的是重发,而重发需要两样东西:知道对方有没有收到、知道什么时候该重发
- UDP 两样都没有------既然永远不会重发,留副本就纯属浪费内存
- 所以「没有发送缓冲区」不是实现偷懒,是设计上根本不需要
不留副本的前提是永远不会重发;UDP 既没有确认也没有重传,发送方向自然不需要缓冲区。
5.2 接收缓冲区带来乱序与丢弃两件事
1. 缓冲区是报文排队等应用层来取的地方
- 数据报到达后先进接收缓冲区排队,应用层什么时候调 recvfrom,什么时候取走一条
- 有这块缓冲区,应用层就不必时时刻刻守着 socket,处理慢一点也不会立刻把数据丢掉
- 换个角度想:如果没有接收缓冲区,报文到达的那一刻应用层就必须已经在等,否则数据没地方放只能扔------缓冲区是把「网络到达的速度」和「应用处理的速度」解耦开的唯一办法
2. 不保证顺序,缓冲区满了就丢弃
- 缓冲区里的报文按到达顺序排队,而网络的到达顺序本来就不保证和发送顺序一致,后发的可能先到
- 缓冲区满之后,新到的数据报直接被丢弃,不通知任何人
3. 同一个 socket 既能读也能写
- 一个 UDP socket 上可以同时调 sendto 和 recvfrom,两个方向互不干扰
- 这个概念叫全双工,TCP 的 socket 同样如此
接收缓冲区是应用层处理速度和网络到达速度之间的垫子,代价是乱序和满则丢弃。
5.3 16 位长度字段把单个数据报锁在 64K 以内
1. 64K 包含 8 字节首部
- 长度字段最大 65535,指的是整条数据报(首部加数据)的长度
- 所以单条 UDP 能带的应用层数据是 65535 减 8,等于 65527 字节
- 放到 IPv4 环境里还要再扣 20 字节 IP 首部,实际能带 65507 字节
2. 超过 64K 只能在应用层自己分包
- 应用层把大数据切成若干块,每块自己加序号,分多次 sendto 发出去
- 接收端按序号收齐,自己拼回原来的数据
- UDP 不会帮你做这件事------分包、编号、拼装、缺块检测,全是应用层的活
64K 不是随手定的数,是 16 位字段能表示的最大值。字段宽度定在哪里,能力天花板就压在哪里。
5.4 基于 UDP 的应用层协议
1. 常见的几个
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议,用于无盘设备启动
- DNS:域名解析协议
2. 它们选 UDP 的共同理由
- 一次请求一次响应,报文小,建连的开销比传输本身还大
- 丢了大不了重来一次,重试的成本远低于维护一条连接
用 UDP 写程序时自定义的那套报文格式,本身就是一份应用层协议,它和上面那几个有名有姓的协议在层次上没有任何区别。
会选 UDP 的协议都有同一个特征:报文小、一问一答、重试比等待便宜。
六、OS 怎么管住同机并存的成千上万个报文:先描述再组织
6.1 服务端单机内一定同时存在大量待处理报文
1. 应用层解析快慢不影响内核收报文
- 应用层还在解析上一条报文的时候,网卡可能又收到十几条了
- 内核不会停下来等应用层,报文照收,收下来的就堆在缓冲区里
- 所以任何一个服务端进程的 socket 缓冲区里、内核协议栈里,任何时刻都可能同时躺着大量报文
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 划出预留区和数据起点
- head 指向这块内存的起始地址
- data 指向当前有效数据的起点------注意是「当前」,因为它会移动
- head 和 data 之间的空隙是留给各层协议头用的,封装时新头就长在这段空隙里
2. tail 与 end 划出数据末尾和内存末尾
- tail 指向当前数据的末尾,end 指向整块内存的末尾
- 数据区在 data 到 tail 之间,整块内存区在 head 到 end 之间
3. 一条报文的内存由报头与有效载荷两部分构成
- 应用层刚交下来时,这块内存里只有有效载荷
- 每向下走一层,就有一个新的报头被填进 data 前面那段预留区
四个指针把一块内存切成三段:预留区留给报头,数据区装有效载荷,尾部空间留着扩容。
6.3 封包与解包本质是移动 data 指针

1. 每向下交付一层,data 前移对应报头大小
- UDP 往下交给 IP 层时,在 data 前面填 20 字节 IP 报头,然后把 data 往前挪 20 字节,让它指向新的有效数据起点
- 再往下交给链路层,填一个以太网头,data 继续前移
- 走到最底下,head 到 data 之间堆满了各层报头,data 到 tail 之间还是应用层那份原始数据
2. 解包时 data 后移,正好相反
- 收到报文,链路层先读自己的头,读完把 data 往后挪,交给网络层
- IP 层读 IP 头,读完再挪,交给传输层
- UDP 拿到之后读前 8 个字节的报头,data 再往后挪 8 字节,剩下的就是应用层数据
3. 接收方的报文以 sk_buff 形态被双链表与队列管理
- 内核收到的是一条 sk_buff,不是一段裸字节
- 每个 socket 的接收缓冲区就是一个 sk_buff 队列,报文到达就入队,recvfrom 就出队

4. UDP 报头与载荷的分离、分用由此统一解释
- 分离:data 指针跳过 8 字节,报头和载荷就分开了------「分离」在实现层面就是挪指针
- 分用:从这 8 个字节里读出目的端口号,查表找到 socket,把 data 指向的那段载荷塞进它的队列
5. 真实内核的 sk_buff 比四指针模型复杂
- 真正的 struct sk_buff 字段远不止四个指针,还带着协议头偏移量、校验和状态、分片信息、路由缓存一大堆成员
- 四指针模型是把「封包解包靠移动指针」这件事抽出来的最小模型,理解机制用它够了,去看真实源码时不要指望只有这四个字段
封包不是把数据一层层拷进新缓冲区,而是让 data 指针在预留空间里向前走;解包就是把指针往后挪回来。
七、全篇总结
1. 传输层的职责是交付到进程
- 传输层负责端到端,交付对象是进程而不是主机
- 五元组(源 IP、源端口、目的 IP、目的端口、协议号)唯一标识一次通信
2. UDP 报头是 8 字节定长结构
- 源端口、目的端口、长度、校验和各占 16 位
- 定长报头让分离不需要分隔符,读掉 8 字节剩下的就是有效载荷
3. 分用靠两级查表
- 协议号决定交给 TCP 还是 UDP
- 目的端口决定交给哪个进程,sendto 因此必须带目的地址
4. 面向数据报保住了报文边界
- 应用层交多长就发多长,不拆分也不合并
- 一次 sendto 对应一次 recvfrom,边界来自定长报头之后那段数据
5. 发送方向不缓存、接收方向要排队
- 没有发送缓冲区,因为不重发就不需要留副本
- 有接收缓冲区,代价是乱序和满则丢弃
6. 内核用 sk_buff 描述并组织报文
- 先描述再组织,四指针是 head、data、tail、end
- 封包解包本质是移动 data 指针
八、文末面试题
8.1 推导题(按本篇知识点,附答案)
-
【推导】UDP 报头里没有「首部长度」字段,接收方怎么知道前 8 个字节是头、后面全是数据?
答:因为报头长度由协议规定死是 8 字节,接收方读满 8 个字节就直接切,不需要报文自己声明头有多长;IP 报头必须有首部长度字段,是因为它的长度可变(带选项时不固定)。
-
【推导】UDP 报头里既没有「首部长度」也没有「序号」,而 TCP 两个都有,为什么?
答:首部长度的差别来自可变性------UDP 首部固定 8 字节不用声明,TCP 首部带选项、长度可变必须自带字段说明;序号的差别来自职责------TCP 靠序号做按序交付和重传判定,UDP 这两件事都不做,留个序号字段也没有任何用途。
-
【推导】sendto 为什么每次都要带目的地址,而 TCP 的 send 不用?
答:UDP 没有连接,报头里的目的 IP 和目的端口只能由本次调用传进来,内核没有别的地方可查;TCP 在建连时已经把对端地址记在连接控制块里,后续 send 直接取用。
-
【推导】UDP 为什么没有发送缓冲区,却必须有接收缓冲区?
答:发送方向不需要缓冲区,是因为不可靠协议不会重发,留副本没有任何用途;接收方向必须有缓冲区,是因为报文到达的时刻由网络决定、应用层取数据的时刻由自己决定,中间需要一块地方先把报文接住,否则报文一到就只能丢弃。
-
【推导】sk_buff 的 data 指针既能表示封包又能表示解包,同一套机制为什么能同时干两件事?
答:因为封包和解包都是「在预留空间里改变有效数据的起点」------封包把 data 向前移、让新报头落在 data 前面,解包把 data 向后移、把已经读过的报头排除在外,两者对 data 的操作方向相反但语义完全一致。
-
【推导】UDP 的接收缓冲区满了以后,新到的报文为什么直接丢,而不是像 TCP 那样让发送方慢下来?
答:因为 UDP 没有流量控制,接收方没有任何手段把「我慢了」这个消息传回发送方,两端之间不存在窗口这回事;缓冲区是这个方向上唯一的缓冲垫,垫满了就只能丢弃,且丢弃后不通知任何人。
8.2 真题(来源已核实,转述注明)
-
UDP 报头有哪些字段?报头和有效载荷是怎么分离、又怎么交付给上层的?
答:固定 8 字节、四个字段各 16 位,依次是源端口号、目的端口号、UDP 长度(整条数据报的长度,含 8 字节首部)、UDP 校验和;分离靠定长,读满前 8 个字节即为报头、其后全是有效载荷;交付靠分用,从报头取出目的端口号,查 socket 映射表找到对应进程再把有效载荷交上去。
【真题·转述自 牛客网《后端面试高频:UDP协议》】
-
UDP 的校验和是怎么算的?算出来对不上会怎么处理?
答:发送方把伪首部(源 IP、目的 IP、协议号、UDP 长度)、UDP 首部和数据一起按 16 位分组做反码求和,结果取反填入校验和字段;接收方用同样的方法重算并比对,不一致就把整条数据报丢弃,而且不会通知应用层。
【真题·转述自 CSDN《UDP协议面试指南》】
-
怎么理解 UDP 的面向数据报?
答:应用层交给 UDP 多长的报文,UDP 就原样发送,既不拆分也不合并;发送端一次 sendto 发 100 字节,接收端就必须一次 recvfrom 收 100 字节,不能循环 10 次每次收 10 字节,读写次数和数量都无法灵活控制。
【真题·转述自 牛客网《后端面试高频:UDP协议》】
-
UDP 会粘包吗?为什么?
答:不会。粘包的前提是协议把数据当成无边界字节流,TCP 正是如此,多次 write 可能被一次 read 读完;UDP 面向数据报,一次 sendto 的数据在接收端正好由一次 recvfrom 完整取出,边界由协议保留,既不会合并也不会拆分。
【真题·转述自 CSDN《UDP协议面试指南》】
-
一条 UDP 数据报最大能传多少数据?业务数据超过这个长度怎么办?
答:报头长度字段 16 位,单条数据报上限 65535 字节,扣掉 8 字节首部后数据区最多 65527 字节,在 IPv4 下再扣 20 字节 IP 首部剩 65507 字节;超过就得在应用层手动分包,每块加序号分多次发送,接收端按序号手动拼装。
【真题·转述自 CSDN《UDP协议面试指南》】
-
DNS 为什么主要用 UDP 而不是 TCP?
答:DNS 查询是一问一答、报文很小,UDP 省掉建连的往返,一次查询一个来回就能出结果,客户端重试的成本也远低于维持一条 TCP 连接;只有在响应超过 512 字节或做区域传送时才会改用 TCP。
【真题·转述自 CSDN《UDP协议面试指南》】
UDP 把能省的全省了,只留下一个 8 字节的报头和一句「送到就到、不到就算」。它不保证的那些事,最后都得由应用层自己补上。