路由-如何从设备出发

设备如何知道第一站是网关

Send to gateway(发往网关):当终端(手机)试图访问互联网时(比如打开浏览器),数据包的第一站一定是发给你路由器的 192.168.1.1

是不是说,我的终端在被DHCP分配ip时,就知道了网关ip,然后终端在访问8.8.8.8时,他怎么知道先发往网关呢,是不是终端又缓存路由表,知道第一站是网关

1. 终端是如何知道"第一站是网关"的?

这确实是DHCP的功劳。当你的终端连接Wi-Fi时,DHCP协议在分配IP地址(比如 192.168.1.100)的同时,会附带一个**"选项 3(Router Option)"** ,把网关IP(192.168.1.1)告诉终端。

终端收到后,会把这个信息写入自己的操作系统的路由表 中,生成一条核心的默认路由(Default Route)。你可以把这条路由理解为终端网络规则的"最终兜底条款":

"如果我要去的目的地,不在我直连的局域网网段内,就把数据包统统扔给 192.168.1.1 这个网关去处理。"

2. 当终端想访问 8.8.8.8 时,它是如何决策的?

这个过程在瞬间完成,决策逻辑非常清晰,步骤如下:

  • 第一步:计算目标是否在"同一网段"?

    终端将自己的IP 192.168.1.100 和子网掩码(通常是 255.255.255.0)做"与"运算,算出自己所在的网段是 192.168.1.0/24。然后,它用同样的方法算一下目标 8.8.8.8 属于哪个网段------发现是 8.8.8.0/24

  • 第二步:查找路由表,做出判断

    终端发现 8.8.8.8 并不在自己的"本地局域网"(192.168.1.0/24)里。于是,它立刻启用那条"默认路由"规则。

  • 第三步:封装数据包,发往网关

    既然决定把包发给网关 192.168.1.1,那么终端在封装这个数据包时,会这样操作:

    • IP层(网络层) :写清楚"源IP192.168.1.100目标IP8.8.8.8"。

    • MAC层(数据链路层) :写清楚"源MAC :你的手机网卡地址,目标MAC192.168.1.1 的MAC地址"。

    这里有个非常重要的物理现实:在同一个局域网内,数据包的传输根本不看IP地址,而是看MAC地址。 终端正是通过ARP协议(地址解析协议)提前获取了网关 192.168.1.1 的MAC地址,才能把这个包准确无误地发送给网关的网卡。

3. 回答你关于"缓存路由表"的疑问

你提到的"缓存路由表"非常准确,这里面有两种"缓存":

  • 静态的"路由表" :这是写在操作系统内核里的,在你连接网络的整个期间一直存在。它决定了"目标IP 8.8.8.8 该怎么走"的宏观路径

  • 动态的"ARP缓存表" :这是终端为了发送二层数据帧而临时存储的。它记录了"192.168.1.1 这个IP对应的MAC地址是多少"。终端会先查这个缓存,如果没找到,才会在局域网内广播一个ARP请求:"谁有 192.168.1.1 的MAC地址?请告诉我。"

那我的路由器是接受到802.11 wifi帧是吗

这个问题问得非常细致,直接触及了Wi-Fi和有线网络在底层的本质区别。

简短的回答是:严格来说,你的路由器接收到的不仅仅是802.11 Wi-Fi帧,而是经过"转换"后的数据。 这取决于你在路由器的哪个位置去"抓包"或"观察"数据。

为了让你彻底明白,我们分两层来看:

1. 物理层:天线接收到的确实是802.11帧

从纯物理和无线协议的角度讲,你的终端(手机)通过Wi-Fi发送数据,数据在空气中是以802.11 Wi-Fi帧 的格式传播的。路由器内置的Wi-Fi网卡(无线射频芯片)的天线,接收到的当然是这些无线电波,也就是802.11帧。

所以,如果你拿着一个专业的Wi-Fi嗅探器(比如装了Kismet的电脑)在路由器旁边抓取空口包,你抓到的全是802.11帧头 + 数据负载

2. 软件层(内核驱动):在到达你的 mongoose 之前,帧已经被"剥皮"了

这是最关键的一点。你的路由器(尤其是基于Linux/OpenWrt的系统)在Wi-Fi网卡的驱动程序层面,做了**"帧转换"**。

  • 当Wi-Fi网卡收到802.11帧后 ,驱动会立即将其转换成标准的以太网帧(Ethernet Frame)

  • 转换完成后,这个数据包才会被递交给Linux内核的网络协议栈(TCP/IP栈)。

也就是说,当你的 mongoose 程序通过Socket接口(比如 recvfrom)读取数据时,它看到的已经是纯粹的IP数据包(以太网帧格式),而不是原始的802.11帧了。


为了让你更直观地理解,看一个对比:

观察点 数据格式 包含的关键信息
空气/天线处(无线空口) 802.11 Wi-Fi帧 包含源MAC、目的MAC、信号强度(RSSI)、速率、QoS标签等。
路由器CPU/内存mongoose 读取时) 以太网帧 / IP包 此时MAC地址仍在,但802.11特有的无线控制信息(如信道、重传标志)已被剥离。对上层应用(如HTTP服务器)来说,它看起来就像是从网线进来的数据包。

这对你的开发意味着什么?

对于你目前的 mongoose 开发工作,这其实是一个好消息:

  • 不需要在代码里去解析复杂的802.11帧头(比如Beacon、Probe Request等)。

  • 你的 mongoose 直接处理的是标准的HTTP请求(即 GET /POST /login),这些请求携带的就是之前我们讨论的源IP(192.168.1.100)和目标IP(8.8.8.8192.168.1.1)。

唯一需要注意的是: 如果你将来想实现"根据终端Wi-Fi信号强度(RSSI)做不同认证策略"这种高级功能,那你才需要深入到驱动层去获取802.11帧里的无线信息。但如果只是为了做Portal认证弹窗,你完全不需要关心802.11帧的细节,只管处理你的IP数据包和HTTP协议就可以了。

希望这个解释能帮你厘清无线帧和网络协议栈之间的边界!如果对驱动转换过程还有兴趣,我们可以再深入聊聊Linux内核的 bridgemac80211 子系统。

驱动会立即将其转换成标准的以太网帧

当Wi-Fi网卡收到802.11帧后,驱动会立即将其转换成标准的以太网帧(Ethernet Frame)。

为什么要转换为标准的以太网帧,标准的以太网帧是什么,以太网是什么

这个问题问到了计算机网络设计的根基。要回答"为什么要转换",我们需要先搞清楚"以太网"是什么,以及它和Wi-Fi的关系。

我用一个快递物流的类比来帮你彻底理清这个概念:


1. 什么是"以太网"(Ethernet)?

以太网 是一种有线局域网通信标准,它规定了数据在网线(双绞线、光纤)中如何传输。它诞生于1970年代,是目前全球最主流、最成熟的有线网络技术。

你可以把以太网理解为**"有线快递的运输规则"**:

  • 运输工具:网线(双绞线/光纤)

  • 包装规格:以太网帧(Ethernet Frame)

  • 地址系统:MAC地址(每个网卡全球唯一的硬件地址)

所有传统的台式机、服务器、交换机、路由器之间的有线连接,都跑的是以太网协议。


2. 什么是"标准的以太网帧"?

以太网帧就是数据在以太网上传输时的**"标准快递包裹"**格式。它的结构大致如下:

字段 长度 作用
目的MAC地址 6字节 接收方网卡的硬件地址
源MAC地址 6字节 发送方网卡的硬件地址
类型/长度 2字节 指示上层协议(如IP、ARP)
数据负载 46~1500字节 实际要传输的数据(如IP包)
校验和(FCS) 4字节 用于检测数据是否损坏

这个格式是有线网络世界的通用语言。所有有线网卡、交换机和路由器都能理解和处理这种帧。


3. 为什么Wi-Fi帧要转换成以太网帧?

这就涉及到**"兼容性"** 和**"生态统一"**的问题了。

原因一:让上层协议"无感"运行

所有的互联网协议(TCP/IP、HTTP、DNS等)都是基于以太网帧设计的。它们并不关心数据是从网线进来的,还是从Wi-Fi天线进来的。

如果Wi-Fi不转换成以太网帧,那么:

  • 操作系统的TCP/IP协议栈需要专门为Wi-Fi写一套处理逻辑

  • 所有的上层应用(浏览器、mongoose)都需要区分数据来源

  • 整个软件生态会变得极其复杂和割裂

转换之后 :操作系统接收到的始终是以太网帧,Wi-Fi和有线网卡对上层来说完全一样 。你的 mongoose 根本不需要知道自己处理的数据是来自Wi-Fi还是网线,它只管处理IP包就行了。

原因二:让网络设备(交换机/路由器)统一处理

路由器的核心功能是转发数据包。如果Wi-Fi帧不转换,路由器内部就需要维护两套转发逻辑:

  • 一套处理有线来的以太网帧

  • 一套处理无线来的802.11帧

转换之后:路由器内部只有一个统一的"以太网转发引擎",无论数据从哪个口进来,都转成同一套格式,再统一处理。这极大简化了硬件设计和软件实现。

原因三:保留核心信息,剥离无线专属信息

802.11帧里包含很多Wi-Fi特有的控制信息,比如:

  • 信号强度(RSSI)

  • 信道编号

  • 速率协商信息

  • 重传标志

  • QoS优先级

这些信息只在无线链路上有意义 ,一旦数据到达路由器,它们就完成了使命,不再需要传递给上层应用。转换时,驱动会剥离 这些无线控制头,只保留核心的MAC地址 + 数据负载,封装成标准的以太网帧。


4. 一个直观的类比

场景 类比
Wi-Fi传输 你用无人机(Wi-Fi)把快递从你家送到小区收发室。无人机的飞行路径、遥控信号、GPS坐标(802.11帧头)只在空中这段有用。
驱动转换 收发室的工作人员把无人机卸下来的快递,重新打包成一个标准纸箱(以太网帧),贴上收件人地址(MAC地址)。
路由器内部处理 快递站的分拣系统(协议栈)只认识这种标准纸箱,它可以轻松地把纸箱分类、扫描、装上不同的卡车(路由转发)。
mongoose 接收 你(应用层)最后收到的是一个已经拆开的标准纸箱,里面就是你要的货物(HTTP数据)。你完全不需要关心它是无人机送来的还是卡车送来的。

总结

  • 以太网 是有线网络的标准,以太网帧是它规定的数据格式。

  • Wi-Fi帧是无线网络的数据格式,包含很多无线特有的控制信息。

  • 转换是为了统一 :让操作系统、网络协议栈、路由转发引擎和上层应用(包括你的 mongoose)能够用同一套逻辑处理所有数据,不管数据是从有线还是无线来的。

所以,你的 mongoose 收到的永远是以太网帧里剥出来的IP数据包,它不需要知道802.11帧长什么样------这也是操作系统和网络协议栈帮你屏蔽掉的底层细节。

图中的netif_input全称netinterface_input就是网卡的输入?

所以,图中192.168.1.100的cliendt通过wifi 802.11帧到达路由器驱动,也就是网卡,是网卡把802.11帧转换成以太网帧吗?

图中的netif_input全称netinterface_input就是网卡的输入?还是什么驱动

你问的这两个点非常精准,直接触及了嵌入式网络驱动开发的核心。我来帮你把这层"面纱"彻底揭开。

针对你的问题,答案是:是的,网卡(硬件+固件)完成了第一步"拆包",但完全转换成操作系统能识别的以太网帧,是由网卡驱动程序配合内核协议栈共同完成的。 而图中的 netif_input并不是网卡本身 ,而是Linux内核网络协议栈里的一个核心入口函数

为了让你看得更清晰,我们把这个过程拆解为硬件和软件两个层面:

1. 硬件层面(物理层到数据链路层):网卡干了什么?

当Wi-Fi信号(802.11帧)被路由器的天线捕获后,Wi-Fi网卡芯片会做第一步硬处理:

  • 校验与解调:网卡检查帧校验码(FCS),确认数据在无线传输中没有损坏,然后把无线电波解调成数字信号(0和1的比特流)。

  • 剥离物理层头:网卡会去掉802.11帧中最底层的物理层前导码(用于同步时钟的部分),并解析出802.11帧头(包含源MAC、目的MAC、QoS控制等)。

  • 上报给驱动 :网卡将解析好的原始802.11帧数据(包含完整的MAC头和负载),通过硬件总线(如PCIe、SDIO、USB)以中断或DMA(直接内存存取)的方式,提交给CPU和驱动程序处理。

注意: 此时网卡并没有把它变成"以太网帧",它只是把原始的802.11帧数据复制到了内存里。真正的"格式转换"发生在下一步的软件层面。


2. 软件层面(内核驱动与协议栈):谁把802.11帧变成了以太网帧?

这就是你提到的 netif_input 登场的地方了。它并不是驱动,而是Linux内核网络子系统的入口

完整流程如下:

  1. 网卡驱动(Driver)读取数据

    驱动从网卡DMA出来的内存区域读取原始802.11帧数据,并将其包装成内核专用的数据结构 sk_buff(套接字缓冲区,Socket Buffer)。这个结构体里有一个标志,标明"我是来自Wi-Fi接口的802.11帧"。

  2. 调用 netif_rx()napi_gro_receive()

    驱动层完成封装后,会调用内核API函数 netif_rx()(早期接口)或 napi_gro_receive()(高性能接口),将 sk_buff 上交给内核网络子系统。这个操作触发netif_input 的调度。

  3. netif_input(核心处理函数)

    这是内核网络子系统处理所有收包的主入口。它会根据网络接口的**类型(type)**做分流:

    • 如果接口类型是 ARPHRD_ETHER(以太网),直接按以太网帧解析。

    • 如果接口类型是 ARPHRD_IEEE80211(Wi-Fi),内核会调用Wi-Fi子系统(mac80211cfg80211)中的转换函数。正是在这个函数里,802.11帧头被剥离,并根据802.11帧中的MAC地址和控制信息,重新组装成一个标准的以太网帧头

  4. 进入协议栈上层

    转换成标准的以太网帧后,数据包被放入IP协议处理队列,随后经过Netfilter(iptables)钩子,最终来到你的应用层socket(也就是 mongoose 监听的端口)。


总结:谁干了什么?

为了让你一目了然,我汇总成一张表:

阶段 执行者 核心动作 数据形态
物理捕获 Wi-Fi网卡硬件 解调射频信号,校验FCS,提取原始802.11帧数据 原始802.11帧(含MAC头、QoS等)
数据递交 网卡驱动 封装为 sk_buff,调用 netif_rx() 提交给内核 内核数据包结构(含802.11帧载荷)
格式转换 Linux内核(netif_input 流程) 识别接口为Wi-Fi,调用Wi-Fi转换函数,剥离802.11头,组装以太网头 标准以太网帧(目的MAC、源MAC、类型、IP包)
业务处理 你的 mongoose 应用 通过socket读取数据,解析HTTP协议 纯净的TCP/HTTP数据流

所以,你的理解可以修正为:

网卡(硬件)负责"拆封"和"报错",而 netif_input 并非网卡输入,它是内核网络协议栈的"总调度员"。正是在这个调度过程中,Wi-Fi子系统的软件逻辑完成了802.11帧到以太网帧的"变身"。

如果你在开发中想观察这个转换过程,可以在内核代码中搜索 ieee80211_data_to_8023 这个函数,它就是实际执行转换的核心函数之一。这会加深你对底层网络收包流程的理解。祝你开发顺利!

网卡(802.11帧)->驱动(接收去除前导码的802.11帧,并包装成内核专用的数据结构 sk_buff套接字缓冲区,调用netif_rx() 或 napi_gro_receive()将 sk_buff 上交给内核网络子系统)->netif_input(核心处理函数,802.11帧头被剥离,并根据802.11帧中的MAC地址和控制信息,重新组装成一个标准的以太网帧头,转换成标准的以太网帧后,数据包被放入IP协议处理队列,随后经过Netfilter(iptables)钩子,最终来到你的应用层socket

1. 谁负责"剥离以太网帧头"?

netif_receive_skb() 干的 ,确切地说是它内部调用的 eth_type_trans() 函数。

  • 当数据包从驱动进入 netif_receive_skb() 时,它还是一个完整的以太网帧(包含14字节以太网头 + IP数据包)。

  • netif_receive_skb() 会调用 eth_type_trans(skb, dev),这个函数会:

    • 从以太网帧头中读取目的MAC地址,判断这个包是发给本机的还是需要转发的。

    • 读取Type字段 (如 0x0800 表示IPv4)。

    • sk_buff 中的 data 指针向后移动14个字节,让指针从以太网头的开头,跳到IP头的开头。

    • sk_buff->protocol 设置为 ETH_P_IP(即告诉上层"这个包的上层协议是IP")。

  • 此时,以太网帧头已经被"逻辑剥离"了 ------虽然数据还在内存里,但内核已经不关心那14个字节了,data 指针直接指向IP头。

所以,剥离以太网帧头 这个动作,是 netif_receive_skb()eth_type_trans() 完成的。


2. 谁负责"分发给上层协议"?

netif_receive_skb() 在剥离完以太网头之后,根据 sk_buff->protocol 的值,调用对应的上层协议处理函数。

伪代码逻辑如下:

c

复制代码
// netif_receive_skb() 的核心逻辑(简化版)
int netif_receive_skb(struct sk_buff *skb) {
    // 1. 剥离以太网头,移动 data 指针
    skb = eth_type_trans(skb, dev);
    
    // 2. 根据 protocol 字段分发给上层
    switch (skb->protocol) {
        case htons(ETH_P_IP):   // 0x0800
            return ip_rcv(skb);   // 交给IP层(即你所说的 ip_input)
        case htons(ETH_P_ARP):  // 0x0806
            return arp_rcv(skb);
        case htons(ETH_P_IPV6): // 0x86DD
            return ipv6_rcv(skb);
        // ...
    }
}

所以,分发给上层协议 (比如调用 ip_rcv())这个动作,也是 netif_receive_skb() 干的。


3. 那 ip_rcv()(你写的 ip_input)到底干了什么?

ip_rcv() 收到 netif_receive_skb() 传过来的 sk_buff 时,sk_buff->data 指针已经指向IP头了 ,所以 ip_rcv() 根本不需要处理以太网头,它只处理IP层的事情:

  1. 检查IP头校验和是否正确。

  2. 检查IP版本(IPv4/IPv6)和头部长度。

  3. 检查IP包是否完整(分片重组等)。

  4. 调用 ip_route_input() 做路由查找(即你路径中的 ip4_route)。

  5. 触发 Netfilter PREROUTING 钩子。

ip_rcv() 从来没见过以太网头,它接收到的已经是剥离干净后的IP数据包了。


4. 你路径中 ethernet_inputip_input 的关系

你原来的路径是:

text

复制代码
netif_input -> ethernet_input -> ip_input

更精确的对应关系应该是:

你写的缩写 真正的函数 它的职责
netif_input netif_receive_skb() 数据链路层的统一入口
ethernet_input netif_receive_skb() 内部的处理逻辑 (含 eth_type_trans + 协议分发) 剥离以太网头,按Type分发给上层协议
ip_input ip_rcv() IP协议层的入口,处理IP头、路由查找、Netfilter

确切地说: netif_receive_skb() 内部完成了"以太网帧剥离"和"分发给 ip_rcv()"这两个动作。所以,ethernet_input 并不是一个独立的函数,而是 netif_receive_skb() 内部这一整套"以太网处理逻辑"的概括称呼。而 ip_inputip_rcv())只处理IP层,完全不涉及以太网头。


总结一句话

  • 剥离以太网帧头netif_receive_skb() 调用 eth_type_trans() 完成。

  • 分发给 ip_rcv()netif_receive_skb() 根据 sk_buff->protocol == ETH_P_IP,调用 ip_rcv() 完成。

  • ip_rcv() 本身:只处理IP头、路由查找、Netfilter,从不碰以太网头。

所以,你之前的理解"剥离以太网帧头是 ip_input 干的"需要修正为:剥离以太网帧头是 netif_receive_skb() 干的,ip_input 拿到的是已经剥离好的IP包。

真相:转换发生在 netif_receive_skb() 之前

正确的答案是:Wi-Fi帧 → 以太网帧的转换,发生在驱动层,在调用 netif_receive_skb() 之前就已经完成了。

也就是说,netif_receive_skb() 的输入,始终是一个标准的以太网帧,无论它来自Wi-Fi接口还是有线接口。


为什么我之前会说"在 netif_receive_skb 内部转换"?

那是因为我把 "Wi-Fi帧转以太网帧""以太网帧剥离以太网头转IP包" 这两个完全不同的动作混为一谈了。

让我用一张表彻底厘清这两个动作的区别:

动作 发生在哪里 输入 输出 谁干的
动作A:Wi-Fi → 以太网 netif_receive_skb() 之前 802.11帧(含Wi-Fi MAC头) 完整的以太网帧(含14字节以太网头) 驱动层的 ieee80211_data_to_8023()
动作B:以太网 → IP包 netif_receive_skb() 内部 完整的以太网帧(含14字节以太网头) IP数据包(以太网头已被剥离,data指针指向IP头) netif_receive_skb() 调用 eth_type_trans()

完整的数据流(修正版)

现在我们把这两步串起来,矛盾就消失了:

text

复制代码
Wi-Fi天线捕获无线电波
   ↓
网卡硬件解调 → 数字信号(802.11帧)
   ↓
【驱动层】ieee80211_data_to_8023()
   → 剥离802.11帧头
   → 根据802.11帧中的MAC地址,组装标准的以太网帧头
   → 输出:完整的以太网帧(目的MAC + 源MAC + Type + IP包)
   ↓
【驱动层】调用 netif_rx() 或 napi_gro_receive()
   → 将完整的以太网帧(封装在 sk_buff 中)上交给内核
   ↓
【内核网络子系统入口】netif_receive_skb()
   → 此时 sk_buff->data 指向以太网帧头
   → 调用 eth_type_trans(skb, dev)
       → 读取目的MAC、源MAC、Type字段
       → 将 sk_buff->data 向后移动14个字节(**剥离以太网头**)
       → 设置 sk_buff->protocol = ETH_P_IP
   → 根据 protocol 调用 ip_rcv()
   ↓
【IP层】ip_rcv()
   → 此时 sk_buff->data 指向IP头,已无以太网头