我们之前的 Socket 网络编程中,我们只知道打开一个文件就可以访问了,我们只停留在了 Socket 编程层面上,那么其底层是什么?我们接下来就需要来学习网络原理!!!

传输层

我们之前的文章其实是按照自顶向下的,所以我们之前的所有工作都是在应用层的!那么应用层我们搞定之后,我们就往下走,也就是下一层 --- 传输层

传输层就有代表性协议两个 --- UDPTCP

在TCP/IP网络模型中,传输层(Transport Layer)的核心职责,是负责为运行在不同主机上的应用程序提供端到端的逻辑通信服务。说得更直白一点,就是确保数据能够从一个进程,可靠或高效地传递到另一个远程进程。

然而,这里有一个非常关键的认知转折点:上层应用程序(应用层)其实无法直接操控物理或逻辑网络,它只能与操作系统内核中的传输层进行交互。

我们平时在代码中调用的 readwriterecvfrom 和 sendto 等系统调用,并不是在直接向网线发送比特流,也不是直接从网卡接收数据包。这些接口的背后,实际上是操作系统为用户态进程暴露的一个“窗口”——这个窗口就是套接字(Socket),而我们操作的对象,则是这个套接字所对应的文件描述符(sockfd)

当我们调用 write 或 sendto 发送数据时,操作系统内核所做的,仅仅是将我们应用层准备好的数据(例如一个完整的HTTP请求报文),从用户态内存缓冲区拷贝到内核态中该套接字对应的发送缓冲区(即传输层的操作缓冲区)。一旦数据拷贝完成,这次系统调用就返回了,它并不保证数据此时已经被发出,甚至不保证已经到达网卡。

至于这份数据什么时候真正被封装成数据段(Segment)或数据报(Datagram)并发送到网络中、发送多少字节、是否要重传,这些全部由操作系统内核的传输层协议(如TCP或UDP)根据当时的网络状况、拥塞控制算法、流量控制策略等自主决定。我们作为应用程序开发者,只能“委托”操作系统去完成发送,而无法“命令”它在某个时刻立即发出。

这一机制对TCP和UDP来说是通用的。

  • 对于TCP而言,write 只是将数据追加到TCP的发送缓冲区,TCP协议栈会根据滑动窗口和拥塞窗口,自主地将其切片、封装成TCP段,然后交给IP层发送。

  • 对于UDP而言,虽然它不提供可靠性,但 sendto 的调用逻辑本质相同:它只是将应用层下发的整个UDP数据报(包括应用层头部的完整报文)从用户空间拷贝到内核空间的UDP发送缓冲区,随后UDP层会为其添加UDP头部,再整体下发给网络层(IP层)进行路由和发送。

不然为什么我们需要进行 socket 系统调用,然后得到一个文件描述符 sockfd?因为后续我们对这个 sockfd 进行 readwriterecvfromsendto 操作时,我们其实只是把数据发送到了这个文件描述符所对应的"某个东西"——也就是操作系统内核为这个套接字维护的收发缓冲区。

也正是因为这样,我们才需要 bind,把这个 sockfd 和本地的地址、端口绑定起来,告诉操作系统这个通信端点对应的身份信息。至于数据真正从网卡发出去、路由到对端、对端接收,全是底层帮我们做好的,底层才是发送到网络的根本,我们上层只是把数据交给操作系统就完事了。

总结来说:从应用层的视角看,我们只负责把数据交到“操作系统家门口”(即套接字缓冲区),而后续的封装、路由、发送,乃至对端的接收,则是由操作系统底层协议栈(传输层、网络层、数据链路层)共同协作完成的“黑盒”工作。这就是为什么我们需要通过系统调用陷入内核,因为只有内核才有权限和能力去真正驱动网络硬件,完成跨主机的数据传输。

再谈端口号

端口号 (Port) 表示了一个主机上进行通信的不同的应用程序;现在我们应该知道了,应用层有协议 --- HTTP,绑定端口80!有协议 -- FTP,绑定端口21......:

上图中,IP 地址用来表明当前需要将报文发送的目标主机,端口号用来将报文交给上层的哪一个目标应用!

所以在 TCP/IP 协议中,用 "源 IP"、"源端口号"、"目的 IP"、"目的端口号"、"协议号" 这样一个五元组来表示一个通信(可以通过 netstat -n 来查看);(我们之前的实验是可以拿到访问服务端的客户端对应的 IP 和端口的)


我们之前就说过,网络通信的本质,其实就是进程间通信(IPC),只不过通信的双方不在同一台机器上,而是跨越了网络。它的标识方式也很直观,就是通过 "源IP:端口" 和 "目标IP:端口" 来唯一确定一个通信会话。

那端口号在这里起什么作用呢?端口号与传输层协议(比如 TCP 或 UDP)配合使用,用来唯一标识一台主机上的某个特定进程或服务。也就是说,IP地址帮你找到那台机器,端口号帮你找到那台机器上的具体哪个程序。

端口号是一个 16 位的数字,范围是 0 ~ 65535,它的分配是有明确规则的:

  • 公认端口(Well-Known Ports):0 ~ 1023,通常为系统服务和常用程序保留,比如 HTTP 是 80,HTTPS 是 443,这些我们平时应该都听过。

  • 注册端口(Registered Ports):1024 ~ 49151,由 IANA 分配注册,用于一些特定的服务。

  • 动态端口(Dynamic Ports):49152 ~ 65535,一般给客户端临时使用,不受固定分配,每次连接随机分配一个就行。

对于这些公认端口号,服务器上通常都会有配置文件来指定它们。我们也可以通过下面这些命令来查看端口号的分配情况:

  • Linux 上可以看 /etc/services 文件

  • 也可以用 netstat -tuln 或 ss -tuln 查看当前系统上哪些端口正在被监听

cat /etc/services

端口号与传输层协议(如 TCP 或 UDP)是相互独立的,这意味着 TCP 和 UDP 可以各自使用相同的端口号而不会产生冲突。例如,TCP 的 80 端口可以用于 HTTP 服务,而 UDP 的 80 端口可以用于其他服务。端口号的这种设计允许网络中的不同服务和应用程序通过相同的端口号在不同协议下运行,从而提高了端口的利用率并避免了冲突。

一个进程可以绑定多个端口号,但一个端口号不能被多个进程绑定!!!【否则网络过来的数据,传到指定的端口处了,并不知道该交付给哪一个进程】

在网络通信中,数据包通过源 IP 地址、目标 IP 地址、协议类型、源端口号和目标端口号这五个数字来识别一个通信会话。这种设计确保了数据能够准确地从发送方传输到接收方的特定进程。

现在,让我们结合下图来进一步说明这一点。图中展示了一个典型的网络通信场景,其中包括一个服务器和多个客户端。服务器的 IP 地址是172.20.100.32,它监听80端口(HTTP 服务的标准端口)。客户端 B 和客户端 A 分别通过不同的端口(2001 和 2002)与服务器通信。

图中展示了三个TCP数据包的示例:

  • 第一个数据包是从客户端 B(IP 地址172.20.100.34,端口2001)发送到服务器(IP 地址172.20.100.32,端口80)的。

  • 第二个数据包是从服务器(IP 地址172.20.100.32,端口80)发送回客户端 B(IP 地址172.20.100.34,端口2001)的。

  • 第三个数据包是从客户端 A(IP 地址172.20.100.33,端口2901)发送到服务器(IP 地址172.20.100.32,端口80)的。

此时的数据使用应用层协议,比如HTTP的报头加上其正文部分,也就是有效载荷,然后向下添加对应的TCP报头,TCP/UDP的报头中会包含源端口目标端口,所以我们之前看到的端口号,其实是传输层中的概念。后面还需要继续向下封装,在网络层添加IP报头,其报头中包含源 IP 地址目标 IP 地址还有协议号,该协议号是为了对端收到报文自底向上分用时,可以根据协议号知道是传给TCP还是UDP……

这些数据包的源 IP 地址、目标 IP 地址、协议号、源端口号、目标端口号这样的五元组共同标识了网络通信中的特定会话。通过这种方式,即使在网络中有多个客户端同时与服务器通信,每个通信会话也能够被正确地识别和管理。

UDP

UDP 协议格式

不管是我们之前在应用层上实现的网络版本计算器,还是我们自己实现的服务端的HTTP最终都是需要序列化成字节流,应用层序列化成字节流后,就形成了相关的报文,需要将报文交给下一层 —— 传输层,交付给下一层就需要添加报头 —— UDP报头,UDP的报头是一种传输层协议!

我们接下来来看看UDP协议端的格式:

每个 UDP 报文分为 UDP 报头和 UDP 数据区两部分;

报头由 4 个 16 位长 (2 字节) 字段组成,分别说明该报文的源端口、目的端口、报文长度和校验值;

UDP 报文中每个字段的含义:

源端口:操作系统自动分配的,这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中。如果不写入端口号,则把这个字段设置为 0;如果这样接收端的应用程序就不能发送响应了;

一次 UDP 通信是双向的:你发:客户端 → 服务器,那么服务器回包:服务器 → 客户端,服务器要回你,必须知道两个东西:一个是你的 IP(从 IP 头里拿),另一个是你的 端口(从 UDP 头的源端口拿),如果你把源端口填 0,那么服务器收到报文,看到源端口是 0,它想回你,却不知道要发去哪个端口,操作系统里没有应用会绑定 0 端口,0 是无效端口,导致结果就是服务器想回也回不了,只能丢掉响应,或者直接发不出去。

源端口填 0 时,发送方不提供有效回信端口,接收端无法回复,从而实现只发不收的单向 UDP 通信,常见场景如设备广播上报状态、日志单向发送、局域网心跳通知、视频音频实时推流这类不需要接收响应的业务。

目的端口:服务器提前准备好的端口,接收端计算机上 UDP 软件使用的端口,占据 16 位;

长度:该字段占据 16 位 (2 字节),表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度,因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8;

校验和:该字段占据 16 位,可以检验数据在传输过程中是否被损坏;【只负责检查数据有没有被改坏,不负责 “丢了重发”】

UDP 的特点

特点 描述
无连接 UDP是无连接的协议,发送数据前不需要建立连接,知道对端的IP和端口号就可以直接传输。(和TCP不一样。TCP是在通信时需要先发起Connect,UDP并没有这个工作,服务器一起来就直接像UDP发送消息,直接sendto)
不可靠 UDP没有确认机制和重传机制。如果数据包在传输过程中丢失,UDP协议层不会给应用层返回错误信息。(我们目前还没有谈论可靠是怎么保证的)(不可靠是UDP的特点,不是缺点)
面向数据报 UDP是面向数据报的协议,每个UDP数据包都是一个独立的信息单元,协议不保证数据包的顺序和完整性。
传输效率高 由于UDP的简单性,它在传输数据时的开销较小,因此传输效率较高。
支持广播和组播 UDP支持广播和组播,可以同时向多个接收者发送数据,适用于需要一对多通信的场景。
头部开销小 UDP头部只有8字节,相比于TCP的20字节(不包括选项字段)要小,减少了传输的额外开销。
应用场景 适用于对传输速度要求高,但可以容忍一定丢包率的应用,如视频流、在线游戏、DNS查询等。

UDP 的这些特点使得它在某些应用场景下非常适用,尤其是在需要快速传输且对丢包不敏感的情况下。然而,由于其不可靠性,UDP 通常需要应用层来实现额外的确认和重传机制,以确保数据的可靠传输。


任何层协议都必须解决两种问题:

如何分离?

因为 UDP 采用的是8 字节定长报头,后面对端传输层收到 UDP 报文之后,直接从整个报文的头部当中直接获取 8 字节,就是其 UDP 的报头,那么剩下的就是 UDP 的有效载荷!所以,我们就很简单的实现了 UDP 的报头和有效载荷的分离!!!反向的,我们就可以实现封装了!!!

如何分用?

UDP 作为传输层协议,上一层就是应用层了,那么我们收到对端发送过来的 UDP 报文,该怎么知道将有效载荷传递给上一层的具体的哪一个协议呢?就是根据目的端口号!!!这就使我们之前使用sendto的时候,需要传参:server_socketserver_ipserver_port的原因!


面向数据报 

UDP 协议中,数据是以数据报的形式传输的,每个 sendto 发送的数据对应一个完整的数据报。接收端必须一次 recvfrom 接收整个数据报,不能像 TCP 那样分多次接收。【很重要!】

具体来说:发送端调用 sendto 发送 100 字节,这 100 字节会作为一个完整的 UDP 数据报发送。接收端必须用足够大的缓冲区一次调用 recvfrom 接收这 100 字节。不能分 10 次、每次 10 字节来接收,因为 UDP 是基于消息边界的协议。

如果接收方的缓冲区小于 100 字节:在大多数实现中,多余的数据会被丢弃。有些系统可能会返回错误(如 EMSGSIZE)。

所以正确的做法是接收方的缓冲区至少要和发送的数据报一样大,并且准备一次接收整个数据报。

内核根据 UDP 协议的特性,在底层维护了消息边界,并通过 recvfrom 系统调用提供给应用程序。

所以端口号为什么是 16 位的?这不就是因为 UDP 的协议就是规定的 16 位嘛!

UDP 凭什么叫做用户数据报?今天对端发送一个 UDP,我这个接收方一定能够读到一个完整的 UDP 报文,因为 UDP 协议中有标识的 16 位 UDP 总长度,还有 8 字节的 UDP 报头长度,这样有效载荷是多少也就是很明了的了!也就是说:有效载荷的长度,在被读取的时候就是确定的了,这也就是报文和报文之间都是有边界的!!!

不像我们写 TCP 的这种面向字节流,其报文和报文的边界,我们在应用层的时候自己实现,而 UDP 不需要我们用户自己实现,因为在内核当中就可以分开了,所以 UDP 叫做面向数据报!!!

协议本质就是结构体呀!我们来看看什么是 UDP 协议!!!

UDP 协议头在内核中的表示方式是通过 udphdr 结构体来定义的,该结构体包含四个字段:源端口、目标端口、数据包长度和校验和。这些字段与 UDP 头部的字段一一对应,用于在内核中处理 UDP 数据包时识别和解析数据包的各个部分。

struct udphdr {
    __u16   source;  // 源端口
    __u16   dest;    // 目标端口
    __u16   len;     // 数据包长度
    __u16   check;   // 校验和
};

这个结构体的字段与 UDP 头部的字段一一对应。在内核中处理 UDP 数据包时,这些字段被用来识别和解析数据包的各个部分。

注意:操作系统之间并不是直接传递内存中的结构体对象,而是按照 UDP 协议格式传输二进制字节流。Windows 和 Linux 虽然都是 C 语言编写的,但不同系统的结构体内存对齐、主机字节序(大小端)存在差异,无法直接互通。因此在发送前,协议栈必须显式做序列化处理:把端口号、长度等字段转为网络字节序(大端),并保证头部是紧凑无对齐填充的 8 字节格式,并非 “自动处理好”。

之所以效率很高,是因为 UDP 头部本身就是一种极简的二进制序列化格式,而不是操作系统 “不做序列化”。

因为数据通常以字节流的形式在网络接口和内存之间传输。在 C 语言中,这些数据通常存储在缓冲区中,而缓冲区的地址由指针表示。操作系统和网络协议栈会使用这些指针来直接访问和组装协议报文。

UDP 的缓冲区

我们之前看过TCP的发送缓冲区还有接收缓冲区:

其实UDP没有真正意义上的发送缓冲区的,因为没有必要,当然也可以有,但是一般都不用:

UDP 没有真正意义上的发送缓冲区。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。因为将 UDP 的数据缓存起来并没有任何意义,因为 UDP 不保证可靠性。举个例子就是,如果是TCP的,是有发送缓冲区的,如果发送的报文在网络中丢失的话,其实就是需要重传了,那么 TCP 在发送报文到网络的时候是不知道这个报文是否会丢包的,所以在 TCP 发送缓冲区在发送报文之后,该缓冲区的数据内容是不能被清除的,不然拿什么去重传,反正丢了就丢了,我们可以重传呀,该缓冲区的相关数据内容是暂时需要保存起来的,在对端收到之后,发现报文完整,就将暂时缓存起来的数据内容清除!

UDP不存在用于重传的发送缓冲区。虽然内核有临时队列承接 sendto 的数据,但不会长久保存报文;sendto 把数据拷贝到内核后,内核尽快下发网络层,不需要留存副本。

UDP 具有接收缓冲区。但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致;如果缓冲区满了,再到达的 UDP 数据就会被丢弃。因为在网络间通信的时候,上层,也就是应用层还需要对收到的报文进行序列化和反序列化,还有继续向上的路由,函数回调等等一大堆工作,说明上层也是很忙的!当读一个报文,上层在处理期间,我们就需要让我们的操作系统具备收取 UDP 报文的能力,所以操作系统提供一个接收缓冲区并不过分,也就是说上层在忙的时候,UDP 的接收缓冲区也是可以缓存一些报文信息的,也是出于效率的考量,不过 UDP 接收缓冲区满了的话,再来一个报文也就将其丢掉了,这也就是为什么说 UDP 是一种不可靠协议!!!

UDP 的 socket 既能读,也能写,这个概念叫做全双工

UDP 使用注意事项

我们注意到,UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)。然而 64K 在当今的互联网环境下,是一个非常小的数字。如果我们需要传输的数据超过 64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。

基于 UDP 的应用层协议:

  • NFS(网络文件系统):用于通过网络访问和管理远程文件系统,允许用户像访问本地文件系统一样访问远程文件。它通常用于局域网中的文件共享,支持多种操作系统。

  • TFTP(简单文件传输协议):一种轻量级的文件传输协议,主要用于在小型设备(如路由器或交换机)之间传输文件。它简单易用,但功能有限,不支持复杂的文件操作。

  • DHCP(动态主机配置协议):用于自动分配 IP 地址和其他网络配置参数给网络设备。它大大简化了网络管理,尤其是在大型网络环境中,能够动态地为设备分配 IP 地址,避免手动配置的繁琐。

  • BOOTP(启动协议,用于无盘设备启动):主要用于无盘设备(如无盘工作站)的启动过程,允许设备从网络上获取启动信息和操作系统映像,从而实现无盘启动。

  • DNS(域名解析协议):用于将域名(如 www.example.com)解析为 IP 地址(如 192.0.2.1),是互联网中不可或缺的基础服务,使得用户可以通过易于记忆的域名访问网站和服务。

当然,也包括我们自己写的 UDP 程序时自定义的应用层协议。这些自定义协议可以根据特定需求设计,用于实现特定功能,如实时通信、游戏数据传输等。

报文的理解

如果应用层正在进行报文的解析,处理等,占用 CPU 的时候,会不会影响 OS 从网络中读取报文?为什么?

其实不会影响的!!!

其实网卡里面一旦有数据,网卡会向操作系统特定的 CPU 针脚触发硬件中断,这个硬件中断属于外设的硬件中断,操作系统就需要转而去执行中断所对应的中断向量表当中的方法,去读取外设当中的数据,这是操作系统的工作!外设也在不断的向 CPU 触发时钟中断,来触发对应进程能够调度起来!所以整个系统就是不断的由大量的外设来驱动对应的 CPU,要么在执行调度,要么在执行操作系统的代码,其实当应用层正在进行报文的解析,处理等,占用 CPU 的时候,是操作系统在不停的响应时钟中断,在做进程的调度与运行,在不断响应时钟中断的同时,也在不断的响应着外设的外部中断,最终我们操作系统就可以既能把他做了,也能把你做了!!!

客户端:服务器 = n : 1!这不就意味着将来在 OS 内部,一定有可能会同时存在大量的报文,而这些报文就需要被管理起来 --- 先描述,再组织!!!

struct sk_buff 数据结构

struct sk_buff 是 Linux 内核中用于表示网络数据包的核心结构体,它不仅存储数据包的内容,还包含大量用于网络协议栈处理的元数据。以下是其主要字段的定义和说明:

struct sk_buff {
    union {
        struct {
            struct sk_buff *next;          // 指向下一个 sk_buff 的指针,用于链表操作
            struct sk_buff *prev;          // 指向前一个 sk_buff 的指针,用于链表操作
            union {
                ktime_t tstamp;            // 时间戳,记录数据包的接收或发送时间
                struct skb_mstamp skb_mstamp; // 用于精确时间戳的结构体
            };
        };
        struct rb_node rbnode;             // 红黑树节点,用于在某些情况下组织 sk_buff
    };
    struct sock *sk;                       // 指向关联的套接字结构体(如果有的话)
    struct net_device *dev;                // 指向关联的网络设备结构体(如网卡)
    char cb[48] __aligned(8);              // 控制块,用于存储协议栈内部的控制信息
    unsigned long _skb_refdst;             // 用于路由目的的引用计数
    void (*destructor)(struct sk_buff *skb); // 当 sk_buff 被销毁时调用的回调函数
#ifdef CONFIG_XFRM
    struct sec_path *sp;                   // 安全路径结构体,用于安全协议(如 IPsec)
#endif
    unsigned int len, data_len;            // 数据包总长度和分片数据长度
    __u16 mac_len, hdr_len;                // MAC 头长度和网络头长度
    kmemcheck_bitfield_begin(flags1);      // 开始标志字段
    __u8 cloned:1,                         // 是否是克隆的 sk_buff
           nohdr:1,                        // 是否没有头部(用于某些特殊操作)
           fclone:2,                       // 克隆类型
           peeked:1,                       // 是否被偷窥(peeked)
           head_frag:1,                    // 是否头部是分片的
           xmit_more:1,                    // 是否还有更多数据要发送
           __unused:1;                     // 未使用的位
    kmemcheck_bitfield_end(flags1);        // 结束标志字段
    __u8 pkt_type:3,                       // 数据包类型(如入站、出站等)
           pfmemalloc:1,                   // 是否由内存分配失败触发
           ignore_df:1,                    // 是否忽略 DF(Don't Fragment)标志
           nfctinfo:3;                     // 网络连接跟踪信息
    __u8 nf_trace:1,                       // 是否启用 Netfilter 跟踪
           ip_summed:2,                    // IP 校验和计算方式
           ooo_okay:1,                     // 是否允许乱序处理
           l4_hash:1,                      // 是否有四层哈希
           sw_hash:1,                      // 是否有软件哈希
           wifi_acked_valid:1,             // WiFi ACK 是否有效
           wifi_acked:1;                   // WiFi 是否已确认
    __u8 no_fcs:1,                         // 是否禁用 FCS(Frame Check Sequence)
           encapsulation:1,                // 是否有封装
           encap_hdr_csum:1,               // 是否有封装头校验和
           csum_valid:1,                   // 校验和是否有效
           csum_complete_sw:1,             // 是否有完整的软件校验和
           csum_level:2,                   // 校验和级别
           csum_bad:1;                     // 校验和是否错误
    __u8 ndisc_nodetype:2,                 // NDISC 节点类型
           ipvs_property:1,                // 是否有 IPVS 属性
           inner_protocol_type:1,          // 内部协议类型
           remcsum_offload:1;              // 是否启用远程校验和卸载
    __u8 offload_fwd_mark:1;               // 卸载转发标记
    __u16 queue_mapping;                   // 队列映射信息
    __wsum csum;                           // 校验和
    __u32 priority;                        // 数据包优先级
    int skb_iif;                           // 数据包进入的接口索引
    __u32 hash;                            // 数据包哈希值
    __be16 vlan_proto;                     // VLAN 协议类型
    __u16 vlan_tci;                        // VLAN 标签控制信息
    union {
        unsigned int napi_id;              // NAPI ID,用于软中断处理
        unsigned int sender_cpu;           // 发送 CPU 标识
    };
    __u32 secmark;                         // 安全标记
    union {
        __u32 mark;                        // 数据包标记
        __u32 reserved_tailroom;           // 预留尾部空间
    };
    union {
        __be16 inner_protocol;             // 内部协议
        __u8 inner_ipproto;                // 内部 IP 协议
    };
    __u16 inner_transport_header;          // 内部传输层头部偏移
    __u16 inner_network_header;            // 内部网络层头部偏移
    __u16 inner_mac_header;                // 内部 MAC 头部偏移
    __be16 protocol;                       // 数据包协议类型
    __u16 transport_header;                // 传输层头部偏移
    __u16 network_header;                  // 网络层头部偏移
    __u16 mac_header;                      // MAC 头部偏移
    sk_buff_data_t tail;                   // 数据尾部指针
    sk_buff_data_t end;                    // 数据结束指针
    unsigned char *head, *data;            // 数据头部和当前数据指针
    unsigned int truesize;                 // 数据包实际大小(包括头部和尾部)
    atomic_t users;                        // 引用计数,用于跟踪数据包的使用情况
};

struct sk_buff 是 Linux 内核网络栈中用于表示网络数据包的核心结构体,它从数据链路层开始就存在,并贯穿整个网络协议栈的处理过程,直到数据被发送出去或被应用程序接收。

这不就是和我们的进程 PCB 很像了嘛😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭 

所谓的封装和解包,本质就是移动 data 指针在缓冲区的位置,就是在加减对应层的协议长度(报头)!!!

比如:封装:

data -= sizeof(struct udphdr);
(struct udphdr*)data->......

分用的时候也是类似,然后将对应的有效载荷放入到接收缓冲区当中!

+=====================================================================================+
|                                      进程 PCB                                       |
|                               struct task_struct (PCB)                               |
+-------------------------------------------------------------------------------------+
|  pid, state, mm, signals, thread_info ...                                           |
|                                                                                     |
|  struct files_struct *files;  ------------------+  文件描述符表                     |
+================================================|=====================================+
                                                 |
                                                 ▼
+=====================================================================================+
|                                文件描述符表 files_struct                            |
+-------------------------------------------------------------------------------------+
|  fd_array[0]  stdin                                 |
|  fd_array[1]  stdout                                |
|  fd_array[2]  stderr                                |
|  fd_array[3]  ------------------> struct file*  <--+  sockfd = 3 就是下标
|  fd_array[4]                                        |
|  ...                                                |
+=====================================================================================+
                                                 |
                                                 ▼
+=====================================================================================+
|                                   struct file                                       |
+-------------------------------------------------------------------------------------+
|  f_ops: socket_file_ops               // socket 类型的文件操作集
|  f_mode, f_flags, f_count
|  private_data: ----------------------> struct sock*  // 【关键绑定】
+=====================================================================================+
                                                 |
                                                 ▼
+=====================================================================================+
|                              内核套接字 struct sock                                 |
+-------------------------------------------------------------------------------------+
|  sk_state, sk_protocol, inet_sock{sport, dport, saddr, daddr}
|
|  // 接收队列:所有网络报文都挂在这里
|  struct sk_buff_head  sk_receive_queue;
|      ├─ struct sk_buff *next;
|      ├─ struct sk_buff *prev;
|      └─ qlen;
|
|  // 发送队列
|  struct sk_buff_head  sk_write_queue;
|
|  // 等待队列:进程 recv 时阻塞在此
|  wait_queue_head_t  sk_wait;  -----------> 休眠的 task_struct(PCB)
+=====================================================================================+
       ↑          ↑          ↑
       │          │          │
       ▼          ▼          ▼
+==========+ +==========+ +==========+ +==========+ +==========+
| sk_buff  | | sk_buff  | | sk_buff  | | sk_buff  | | sk_buff  |
+==========+ +==========+ +==========+ +==========+ +==========+
|  sk ---->| |  sk ---->| |  sk ---->| |  sk ---->| |  sk ---->|  全都指向同一个 struct sock
|  next    | |  next    | |  next    | |  next    | |  next    |
|  data    | |  data    | |  data    | |  data    | |  data    |
|  len     | |  len     | |  len     | |  len     | |  len     |
|  mac_hdr | |  mac_hdr | |  mac_hdr | |  mac_hdr | |  mac_hdr |
|  ip_hdr  | |  ip_hdr  | |  ip_hdr  | |  ip_hdr  | |  ip_hdr  |
|  udp_hdr | |  udp_hdr | |  udp_hdr | |  udp_hdr | |  udp_hdr |
+==========+ +==========+ +==========+ +==========+ +==========+
       ↑
       │
+=====================================================================================+
|                                      网卡硬件                                       |
|   收到数据包 → 硬件中断 → 驱动分配 skb → 填充各层头部 → 入队 sock 接收队列
+=====================================================================================+

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐