☰
【Linux网络】传输层协议UDP、报文封装过程
2026/11/6 1:47:38 网站建设 项目流程

hello~ 很高兴见到大家! 这次带来的是Linux网络中关于网络基础这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙


文章目录

  • 一、UDP
    • 1.1 认识知名服务器端口号
    • 1.2 UDP 协议报文格式
    • 1.3 UDP特点总结
    • 1.4 理解报文封装过程
    • 1.5 理解报文和报文的读取

代码:HTTP代码


一、UDP

  1. 应用层往下,就进入了位于操作系统内部的传输层和网络层。其中,传输层最有名的两个协议就是 TCP 和 UDP 协议(网络层最核心的则是 IP 协议)。

1.1 认识知名服务器端口号

  1. 0 ~ 1023:知名端口号(Well-known Ports)
    像 SSH(22)、HTTP(80)、HTTPS(443) 这些广为使用的应用层协议,它们的端口号都是固定的。
  2. 1024 ~ 65535:一般由普通程序或客户端使用。
    可以细分为两段:
    1024 ~ 49151(注册端口):分配给普通的服务端程序。比如你写的后端代码(如 8080),或者 MySQL(3306)、Redis(6379)等。
    49152 ~ 65535(动态/私有端口):操作系统真正“动态分配”给客户端程序的端口号。当你用浏览器打开网页时,操作系统会从这个范围里随机挑一个空闲的端口(比如 51234)给你的浏览器使用。
  3. 有一些服务器提供的服务是非常常用的,人们约定这些服务会绑定一个固定的端口号(被称为“知名端口”),比如:
  1. SSH 服务器,使用 22 端口(用于安全的远程登录)
  2. FTP 服务器,使用 21 端口(用于文件传输)
  3. Telnet 服务器,使用 23 端口(早期的明文远程登录,现已被淘汰)
  4. HTTP 服务器,使用 80 端口(明文 Web 服务)
  5. HTTPS 服务器,使用 443 端口(加密 Web 服务)
  6. DNS:使用 53 端口(域名解析)
  1. 一个进程可以 bind 多个端口号吗?可以的。 一个进程可以创建多个 Socket,并分别绑定不同的端口号(例如一个 Web 服务器进程同时监听 80 端口和 443 端口)。但是,默认情况下,一个端口号只能被一个进程进行绑定。 如果第二个进程尝试绑定同一个端口,操作系统会报错(比如 Address already in use)。

1.2 UDP 协议报文格式

  1. UDP 报文由源端口号、目的端口号、UDP长度以及UDP校验和组成,其中UDP长度指的是报头+有效载荷总体的大小。校验和则是用来检测数据在传输过程中有没有发生损坏。这四个字段每个占 2 个字节(16位),所以报头固定是 8 字节。
  2. UDP 协议报文的报头长度是固定的,为8字节,而报文总体长度又被记载在报头里面,这样就能够知道有效载荷长度,从而将报头和有效载荷进行分离。
  3. 这些有效载荷应该给谁?–>16位目的端口号,交给这个目的端口号对应的进程就好。
  4. 为什么我们socket的时候端口号得是16位的?是因为这是UDP(和TCP)协议明确规定的。16 位二进制能表示的最大数字是 2 的 16 次方,也就是 65536。这就是为什么网络端口号的范围只能是 0 ~ 65535。
  5. 因为UDP长度是16位的,也就是说一个UDP报文可以传输的有效载荷是有限的(最大约64KB)。如果要传输的内容太长,就得分成好几个报文进行发送(或者底层IP协议自动分片),接收方还得一一拼接。
  6. UDP 报文不存在数据粘包问题,因为读取UDP报文时,必须得读取一整个报文(用户数据报协议就是面向报文的),这个过程很像寄快递或者邮件,寄和收都是一整个。这就是 UDP 的“面向报文”特性。你调用一次 sendto,对方就必须调用一次 recvfrom 来接收。边界非常清晰,就像拿刀子切好的蛋糕,一块就是一块。而 TCP 像水龙头,水流出来是连在一起的,你得自己找分隔符(这就是 TCP 为什么有粘包问题)。
structudphdr{__be16 source;// 源端口号__be16 dest;// 目的端口号__be16 len;// UDP长度__sum16 check;// 校验和};
  1. UDP 报头的本质其实就是一个约定好的协议结构体,由于UDP协议是在操作系统内部的,操作系统大部分是由C语言写的,所以这个协议结构体也是C语言写的。

1.3 UDP特点总结

  1. 无连接:不需要像 TCP 那样通过“三次握手”建立连接,只要知道目标的 IP 和端口号,就可以直接进行数据传输。UDP 在发送数据前不会在双方之间建立任何“状态”或“通道”。它的开销极小(只有 8 字节报头),没有建立和断开连接的延迟,非常适合对实时性要求极高的场景(如直播、DNS 查询)。当然,因为没有连接,它也就无法保证发送顺序和到达顺序一致(乱序)。
  2. 不可靠:UDP 不会告诉你此次传输是否成功。它没有确认机制,也没有重传机制,如果因为网络拥塞等原因导致数据没有送达目标,UDP 也不会向应用层返回任何报错信息。在计算机网络中,UDP 被称为“尽最大努力交付(Best-effort delivery)”。无论数据包是在半路丢了,还是因为校验和错误被接收方内核静默丢弃,发送方都无从知晓。如果应用层(比如你写代码)需要保证数据绝对不丢,必须自己在应用层实现一套“确认+重传”的逻辑(比如 QUIC 协议就是这样做的)。
  3. 面向用户数据报:UDP 是“面向报文”的,它保留了应用层传递消息的边界。应用层交给 UDP 多大的数据,UDP 就原样封装发送;接收方必须一次性读取一整个数据报,不能像 TCP 那样“流水式”地分批读。
    如果接收方的缓冲区太小,装不下整个报文,多余的数据会被直接截断丢弃;如果缓冲区很大,一次也只能读到一个完整的报文,不会把两次发送的数据拼在一起。正因为它严格绑定了“一收一发”的边界,所以读写起来不如 TCP(字节流)那样灵活。
  4. DP 没有真正意义上的发送缓冲区,它调用 sendto 之后,数据会直接拷贝给系统内核,然后由内核交给网络层。它有接收缓冲区,但无法保证发送顺序和到达顺序一致(可以这样理解:几个人虽然起点和终点相同,但因为选择的代步工具不同、路况不同,到达的顺序自然会和出发顺序有所不同)。而且,如果接收缓冲区满了,后续到达的报文就会被直接丢弃。
  5. UDP 的 Socket 既能读也能写,这种特性在网络通信中被称为全双工(Full-Duplex)。也就是说,发送数据和接收数据使用的是两条独立的通道,不需要像对讲机那样“说完一句,切一下开关才能听”。

1.4 理解报文封装过程

  1. 报文的封装过程:
  1. 开辟缓冲区:当应用层想要发送数据时,操作系统内核会先分配一块足够大的连续内存(缓冲区),并预留出足够的“头部空间(Headroom)”。
  2. 拷贝数据:把用户真正要发的数据(比如你写的 HTTP 报文),拷贝到这块内存的靠右的位置。此时,内存左边全是空的。
  3. 指针左移:当数据要往下传给传输层时,内核不会重新分配内存去拷贝数据,而是把指向数据的指针向左移动(比如移动 8 个字节,对应 UDP 报头的大小)。
  4. 强转填充:*把左移后的指针强转成 UDP 协议的结构体指针 (struct udphdr),直接在这 8 个字节里填入源端口、目的端口、长度、校验和。瞬间,一个 UDP 报文就封装好了!
  5. 继续下探:传给网络层(IP)时,指针继续左移 20 字节(IPv4 报头),强转填充 IP 地址;传给链路层(以太网)时,指针再左移,填入 MAC 地址。

1.5 理解报文和报文的读取

  1. 如何理解报文?报文宏观视角:报文就是网络中传输的独立数据块。微观视角:报文就是一段连续的内存。在底层(C语言中),它就是一个“结构体头 + 有效载荷”。
  2. 系统中肯定不只存在一个报文,而是很多个,那么操作系统应该如何管理这些报文?先描述,再组织。先描述形成结构体sk_buff,再组织起来。


  1. 在早期的 Linux 系统版本(如 2.6 版本)中,sk_buff 结构体的核心成员变量主要就是 4 个指针(head、data、tail、end)。其中,data 指针就是封装过程中用来向左移动并填充协议头字段的核心指针。从 head 到 end 就是一整个系统为报文开辟的缓冲区,其中,头空间(headroom)和尾空间(tailroom)通常都是空余的。

  2. 当我们收到一个报文时,应用层之所以能通过文件管理(比如调用 read 或 recvfrom)读取到数据,是因为底层的配合:
    首先,struct file 结构体里面的 private_data 指针指向了此文件的 struct socket 结构体;socket 结构体里面不仅有一个 file 指针回指 struct file(实现互相指),它本身还有一个 struct sock* 类型的基类指针。
    当我们创建的是 UDP 协议时,这个 struct sock* 实际上指向的是更庞大的 udp_sock 结构体。这就像 C 语言中用“基类指针指向子类对象”。因为 udp_sock 的内存布局开头就是 struct sock,所以只要拿到底层的 sock 指针,直接通过指针强转 (struct udp_sock *)sk,就能随意访问 udp_sock 里特有的成员变量了。
    这样,内核就能通过文件管理系统层层剥茧,最终找到数据。在 struct sock 结构体里面,维护着接收队列(sk_receive_queue)和发送队列(sk_write_queue),这两个队列里面挂着的就是一长串的 sk_buff(用来管理报文的结构体),真正的网络数据就藏在 sk_buff 所指向的内存里。

  3. struct socket 是内核中的基础套接字抽象层。调用 socket() 时,内核会创建 struct socket,并关联具体的协议控制块:UDP 对应 udp_sock,TCP 对应 tcp_sock。这些具体结构体内部都内嵌了通用的 struct sock,因此可以通过 struct sock * 这样的基类指针统一访问,并通过函数指针表调用具体协议的实现;需要访问协议特有字段时,再强转回具体类型即可。

  4. 从结构层次看,UDP 为 udp_sock -> inet_sock -> sock;TCP 为 tcp_sock -> inet_connection_sock -> inet_sock -> sock。也就是说,TCP 比 UDP 多一层 inet_connection_sock,而不是多一层 inet_sock,因为两者都包含 inet_sock。Linux 内核正是用这种“内嵌结构体 + 函数指针表”的方式,在 C 语言中模拟继承与多态。


今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询