
1. 双网共存这件事到底在解决什么问题第一次接到“双网共存”这个需求的时候我脑子里第一反应是不就是插两张卡、两个网口都通吗能有多复杂。真正上手调过一轮之后才发现这里面的坑比想象中深得多。MTK 平台在 Android 9.0 上的网络框架虽然已经比较成熟但“双网同时在线、各走各的路由”这个场景官方默认策略并不是开箱即用的需要动到路由表、网络评分、绑定策略好几个层面。先把概念说清楚。所谓双网共存指的是设备上同时存在两套可用的网络通道常见组合是 Wi-Fi 以太网、Wi-Fi 蜂窝、双以太网口并且这两套通道能够同时保持连接状态业务数据可以按需走指定的通道而不是“后连上的把先连上的顶掉”。这跟“双卡双待”不是一回事双卡双待说的是两张 SIM 卡都能注册网络而双网共存强调的是数据通道的并行与分流。这个能力在哪些场景下真正被需要我梳理了一下实际接触过的几类工业控制设备需要一路网口接内网采集数据、另一路接外网做远程维护商用显示终端要求有线网保证稳定推流、Wi-Fi 作为备份链路随时接管还有一些车载或自助设备主通道走以太网辅助通道走 Wi-Fi 做本地调试和内容同步。这些场景的共同点是两条链路各有各的用途不能互相踢掉也不能让流量乱跑。适合谁来读这篇内容如果你是在 MTK 平台上做系统定制、网络框架适配的工程师或者正在处理“插上网线 Wi-Fi 就断”“两个网口只有一个能通”这类问题那这篇基本就是给你写的。我会从整体设计思路讲起把路由表、网络评分、绑定策略这几个核心点拆开再给出一套可以照着改的配置流程最后把我踩过的坑整理成排查表。需要说明的是下面涉及的具体文件路径和参数值是基于 MTK Android 9.0 常见代码结构的合理推断不同项目分支可能有细微差异你落地时以自己代码库为准。2. 整体设计思路为什么不能只改一个地方2.1 先理解 Android 9.0 的网络管理骨架Android 从 7.0 开始逐步把网络管理收拢到ConnectivityService这个核心服务里到了 9.0网络的选择和评分机制已经相当体系化。简单打个比方系统里每张网卡就像一个候选人ConnectivityService是评委它给每个候选人打分NetworkScore分数最高的那个成为“默认网络”应用不指定的话就走它。这个评分机制带来的直接后果就是——默认只有一个网络能当主角其他的只能当备胎。所以双网共存的第一层矛盾就在这里系统天生倾向于“选一个最好的”而我们要的是“两个都留着干活”。光把两个网卡都 up 起来没用因为路由表里默认路由只有一条DNS 也只有一套流量最终还是会往评分高的那个网络跑。2.2 三条主线连接保持、路由分流、应用绑定我把整个方案拆成三条主线来理解这样改起来不容易乱。第一条是连接保持。要确保两张网卡在底层都处于 connected 状态不会因为评分低就被系统主动断开或者标记为不可用。这一层涉及网络工厂NetworkFactory的评分逻辑和ConnectivityService的回收策略。第二条是路由分流。两个网络都连着但数据包得知道从哪个口出去。这就要动 Linux 层的路由表给不同的目的地址或者不同的应用打上标记让它们走不同的路由规则。Android 的网络栈底层还是 Linuxip rule和ip route这套东西是绕不开的。第三条是应用绑定。系统层面路由分好了还得让具体的应用知道该用哪个网络。Android 提供了ConnectivityManager.bindProcessToNetwork()这类接口应用可以主动绑定到某个Network对象上绑定之后这个进程的 socket 默认就走那个网络。这三条主线是层层递进的关系连接不保持后面都是空谈路由不分流两个网络会打架应用不绑定业务代码没法精确控制走哪条路。下面逐个展开。2.3 方案选型的取舍为什么不用简单的“关掉评分”有人可能会想既然评分机制碍事那我把评分逻辑改掉让两个网络分数一样高不就行了。这个思路我试过结论是治标不治本。评分相同的情况下系统在选默认网络时行为不确定可能这次选 A 下次选 B反而更难控制。而且评分机制牵扯到网络切换、断线重连等一堆逻辑粗暴改分数容易引发连锁问题。更稳妥的做法是保留评分机制的正常运转但通过路由策略和应用绑定把“分流”这件事从系统默认行为里剥离出来。也就是说系统爱选谁当默认网络随它去我们关心的是给需要走特定网络的业务单独铺一条路。这样对系统原有逻辑的侵入最小稳定性也最好。这个取舍贯穿了整个方案后面所有配置都是围绕它展开的。3. 核心细节拆解路由表、评分与绑定策略3.1 路由表与路由规则的基本玩法Linux 的路由体系里有两个关键概念路由表table和路由规则rule。默认情况下所有流量查的都是 main 表但我们可以建额外的表然后用规则把特定流量导向特定的表。打个生活化的比方main 表是小区默认的快递柜所有快递默认放那儿但你可以给某些住户单独指定一个柜子额外路由表再在门口贴张告示路由规则说“某某户的快递放二号柜”。Android 9.0 里每个网络在建立时系统会自动分配一个路由表表号通常跟网络的 netId 挂钩。我们可以利用这个机制把某个网络的出口路由单独维护然后通过ip rule把带特定标记的流量指过去。关键命令大概长这样# 查看当前所有路由规则 ip rule show # 查看指定路由表的内容假设表号 1001 ip route show table 1001 # 添加一条规则带 mark 0x64 的流量查表 1001 ip rule add fwmark 0x64 lookup 1001 # 在表 1001 里加默认路由走 eth0 网关 ip route add default via 192.168.1.1 dev eth0 table 1001这里fwmark是防火墙标记配合 iptables 使用。业务流量在出去之前先被 iptables 打上标记然后路由规则根据标记把它导向对应的表。这套组合拳是双网分流的技术底座。注意路由表和规则的顺序很关键。ip rule是按优先级从上往下匹配的先匹配到的生效。新加的规则如果优先级数字比已有的默认规则大可能永远轮不到它。加规则时务必用ip rule show确认顺序。3.2 网络评分机制里必须关注的几个点前面说了保留评分机制但不代表可以完全不管它。有几个地方必须确认否则连接保持这一关就过不了。第一是网络工厂的评分。MTK 平台上 Wi-Fi 和以太网的 NetworkFactory 各自有评分逻辑以太网通常评分较高比如 70Wi-Fi 稍低比如 60。这个分数决定了谁是默认网络但只要两个网络都能保持 connected分数高低本身不影响分流。要确认的是低分网络不会因为“有更高分的网络存在”而被主动 teardown。这个行为跟具体代码分支有关需要在ConnectivityService的网络回收逻辑里确认没有“只保留最高分网络”的激进策略。第二是网络能力NetworkCapabilities的声明。每个网络在注册时会声明自己具备哪些能力比如NET_CAPABILITY_INTERNET、NET_CAPABILITY_NOT_RESTRICTED等。如果某个网络没有声明 INTERNET 能力系统可能认为它不能上网从而不把它当作可用网络。双网共存场景下两个网络都应该正确声明自己的能力集否则应用绑定过去会发现“绑了个寂寞”。第三是多网络并存时的通知与回调。应用通过registerNetworkCallback监听网络变化时默认可能只收到默认网络的通知。如果业务需要感知两个网络的状态注册时要带上合适的NetworkRequest把需要的传输类型TRANSPORT_WIFI、TRANSPORT_ETHERNET都包含进去才能同时收到两边的回调。3.3 应用绑定的两种粒度应用绑定网络有两种粒度用哪种取决于业务复杂度。进程级绑定是最简单的方式调用bindProcessToNetwork(network)之后这个进程里所有未显式指定网络的 socket 都走绑定的网络。适合那种“整个应用就该走某条链路”的场景比如内网采集程序整体走以太网。缺点是粒度粗一个进程里想同时走两个网络就做不到了。Socket 级绑定更精细通过network.getSocketFactory()拿到绑定该网络的 SocketFactory用它创建的 socket 就固定走那个网络。适合一个应用里不同模块走不同链路的场景。代码大概是这样ConnectivityManager cm (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks cm.getAllNetworks(); for (Network network : networks) { NetworkCapabilities caps cm.getNetworkCapabilities(network); if (caps ! null caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { // 拿到以太网网络创建绑定该网络的 socket SocketFactory sf network.getSocketFactory(); Socket socket sf.createSocket(192.168.1.100, 8080); // 这个 socket 的流量固定走以太网 } }两种粒度可以混用进程级绑定做兜底关键 socket 再做单独绑定覆盖。实际项目里我一般建议能用 socket 级绑定就别用进程级因为进程级绑定的副作用是全局的一旦绑错整个进程的网络行为都会异常排查起来很痛苦。3.4 MTK 平台特有的适配点MTK 的网络框架在原生 Android 基础上做了一些定制双网场景下有几个地方要特别留意。一是以太网服务的管理方式。MTK 平台上以太网通常由EthernetManager或类似的系统服务管理网口的 up/down、DHCP 获取、静态 IP 配置都走这套服务。双网共存时要确认以太网服务不会因为 Wi-Fi 连上就主动把网口关掉这个行为在部分定制版本里是存在的。二是网络优先级配置。MTK 的框架里可能有网络优先级的配置文件或属性决定 Wi-Fi 和以太网同时存在时的默认选择。这个配置影响的是“谁是默认网络”不影响分流但如果你希望默认走以太网、Wi-Fi 做辅助就要确认这个优先级配置符合预期。三是底层驱动的双口支持。如果设备有两个物理网口要确认驱动层两个口都能独立工作MAC 地址不冲突中断不共享导致互相干扰。这一层的问题往往表现为“两个口都 up 了但只有一个能收发包”排查时先看ifconfig和dmesg里的驱动日志。4. 实操过程从零配出一套可用的双网分流4.1 环境确认与前置检查动手之前先把现状摸清楚这一步能省掉后面大量返工。我习惯按下面的顺序过一遍确认两个网络接口都能独立工作。先单独插网线测以太网再单独连 Wi-Fi 测无线确保各自单独使用时都正常。如果单独用都有问题双网肯定更不行。确认系统版本和网络框架版本。getprop ro.build.version.release确认是 9.0再看一下ConnectivityService相关代码分支确认没有特殊的定制逻辑。确认 root 权限可用。改路由表、加 iptables 规则都需要 root调试阶段用 adb root 即可量产方案要把这些配置固化到系统启动流程里。记录两个网络的网段和网关。比如以太网 192.168.1.0/24 网关 192.168.1.1Wi-Fi 192.168.2.0/24 网关 192.168.2.1。网段不能冲突冲突了路由会乱。前置检查里有个容易忽略的点DNS 配置。两个网络各自有 DNS系统默认只用默认网络的 DNS。如果业务需要走非默认网络做域名解析得单独给那条链路配 DNS 或者用 IP 直连。这个后面排查章节还会提到。4.2 保持双网络连接状态先解决连接保持。核心是确认两个网络在ConnectivityService眼里都是“可用”状态。用下面的命令观察# 查看当前所有网络及其状态 adb shell dumpsys connectivity | grep -A 5 NetworkAgentInfo # 或者更直接地看网络列表 adb shell cmd connectivity list如果发现 Wi-Fi 连上后以太网被标记为 disconnected 或者直接消失就要去查网络工厂的回收逻辑。常见原因是某个网络的评分低于阈值后被主动释放。处理方式是在网络工厂的评分回调里保证两个网络的分数都高于系统的最低可用阈值同时确认没有“新网络上线即释放旧网络”的逻辑。另一个要确认的是网络请求的匹配。系统里可能有默认的NetworkRequest比如默认上网请求它只会匹配一个网络。双网场景下要确保这个请求不会导致另一个网络被判定为“无人使用”而回收。可以在dumpsys connectivity的输出里看每个网络的requests列表确认两个网络都挂着至少一个请求。实操心得调试连接保持时我习惯先只连一个网络用dumpsys把它的完整状态打印出来存一份再连第二个网络对比差异。差异点往往就是问题所在比盲猜高效得多。4.3 配置路由分流规则连接保持确认之后开始配路由。假设我们要实现目的地址属于 10.0.0.0/8 的流量走以太网其余走 Wi-Fi。步骤如下。第一步确认两个网络各自的路由表号。Android 给每个网络分配的表号可以从ip rule show里看到通常跟 netId 相关。假设以太网对应表 1001Wi-Fi 对应表 1002。第二步在以太网的路由表里确保有到目标网段的路由ip route add 10.0.0.0/8 via 192.168.1.1 dev eth0 table 1001第三步用 iptables 给目标流量打标记。这里按目的地址匹配iptables -t mangle -A OUTPUT -d 10.0.0.0/8 -j MARK --set-mark 0x64第四步加路由规则让带标记的流量查以太网的表ip rule add fwmark 0x64 lookup 1001 priority 100第五步验证。用ip route get查一个目标地址看走哪个口ip route get 10.1.2.3 # 输出里应该显示 dev eth0说明走了以太网 ip route get 8.8.8.8 # 输出里应该显示 dev wlan0说明走了 Wi-Fi这套配置的逻辑是iptables 在 OUTPUT 链上给去往 10.0.0.0/8 的包打标记路由规则根据标记把它们导向以太网的路由表其余没打标记的包继续走默认规则也就是 Wi-Fi。整个过程对应用透明应用不需要改代码。如果分流依据不是目的地址而是源应用iptables 的匹配条件换成-m owner --uid-owner uid即可其余不变。按应用分流在实际项目里更常见因为业务方通常关心的是“我这个应用走哪条路”而不是“我去哪个地址走哪条路”。4.4 应用层绑定与验证路由配好之后应用层可以选择性地再做绑定双保险。验证分两步。先验证系统层分流是否生效。写个简单的测试程序分别向两个网段的目标发请求用tcpdump抓包确认出口网卡# 在以太网口抓包看是否有去往 10.x 的流量 tcpdump -i eth0 -n host 10.1.2.3再验证应用绑定是否生效。在应用里调用bindProcessToNetwork绑定到 Wi-Fi 网络然后访问一个公网地址用tcpdump确认流量从 wlan0 出去。如果绑定后流量还是从 eth0 走说明绑定没生效常见原因是绑定的Network对象已经失效网络重连后对象会变或者绑定调用时机太早网络还没建立。这里有个细节值得说网络重连后Network对象会变化。应用如果缓存了旧的Network对象重连后绑定就失效了。正确做法是注册NetworkCallback在onAvailable回调里拿最新的Network对象再绑定。这个坑我在两个项目里都踩过表现是“刚启动时好好的过一会儿网络切换了一次就全走默认网络了”。4.5 把配置固化到系统启动流程调试阶段的命令都是手敲的量产必须固化。固化的位置通常有两个选择一是写进init.rc或相关的 init 脚本开机时执行二是在网络建立的回调里动态配置因为路由表号可能随 netId 变化动态配置更稳妥。我倾向于动态配置为主、init 脚本兜底。动态配置在ConnectivityService或网络工厂的网络建立回调里做拿到网络对象后立即配路由和规则init 脚本负责一些静态的、不随网络变化的配置比如 iptables 的基础规则。这样即使网络重连、表号变化路由也能跟着更新不会出现“开机正常、重连后失效”的问题。固化时还要注意清理逻辑。网络断开时要把对应的路由规则和 iptables 规则删掉否则规则越积越多可能影响后续网络建立。删除时按规则的具体内容匹配删除不要图省事用 flush容易误伤系统自己的规则。5. 常见问题与排查技巧实录5.1 双网都连着但只有一个能通这是最高频的问题。排查顺序我总结成一张表现象可能原因排查命令处理方式一个网络无默认路由该网络路由表里缺 default 路由ip route show table 表号补加 default 路由流量不按预期走路由规则优先级被覆盖ip rule show调整规则 priority应用绑定无效Network 对象已失效打日志看对象是否变化在回调里重新绑定DNS 解析失败非默认网络没有 DNS 配置getprop net.dns1单独配置该链路 DNS网口 up 但不通驱动层或 DHCP 问题dmesg、ifconfig查驱动日志和 DHCP 租约重点说两个。路由规则优先级被覆盖这个坑很隐蔽因为规则加进去了、ip rule show也能看到但就是不生效。原因是系统自己的规则优先级数字更小先匹配上了。解决办法是把我们的规则 priority 设得比系统规则更小数字更小优先级更高或者确认系统规则不会匹配到我们的流量。DNS 问题也经常被忽略。双网分流做的是 IP 层的事域名解析还是走默认网络的 DNS。如果业务需要走非默认网络访问域名要么给那条链路单独配 DNS 并用resolv.conf或 Android 的 DNS 接口指定要么业务层直接用 IP。我一般建议后者省事且可控。5.2 网络切换后分流规则失效前面提过Network对象会变这里展开说。Android 在网络重连时会创建新的Network对象netId 可能变化对应的路由表号也跟着变。如果路由规则里写死了表号重连后就指向了不存在的表分流自然失效。处理思路是不要在规则里写死表号而是在网络建立回调里动态获取当前表号再配规则。获取表号的方式可以从Network对象关联的LinkProperties里拿或者直接解析ip rule show的输出匹配接口名。动态配置虽然代码多一点但稳定性好得多。另一个相关问题是规则残留。旧网络的规则没删干净新网络的规则又加上去可能出现两条规则匹配同一流量行为不确定。所以网络断开时的清理逻辑一定要写而且要在onLost回调里同步执行不能异步延迟否则清理和新配置可能交错。5.3 应用绑定后仍然走默认网络这个问题的排查我有个固定套路按顺序过确认绑定的 Network 对象当前是 available 状态。用getNetworkCapabilities查一下返回 null 说明对象已失效。确认绑定调用的时机。在网络还没建立时绑定绑定会失败且不报错静默失效。确认 socket 的创建方式。如果 socket 是在绑定之前创建的绑定对已存在的 socket 不生效只对绑定之后新建的 socket 生效。确认没有其他代码覆盖绑定。进程里如果有多处调用bindProcessToNetwork后调用的会覆盖先调用的排查时全局搜一下这个调用点。第 3 点特别容易中招。很多网络库会在初始化时就创建好连接池等业务调用时复用这时候绑定根本作用不到那些已存在的连接上。解决办法要么在绑定之后重建连接池要么改用 socket 级绑定在创建每个 socket 时指定网络。5.4 双网共存下的功耗与稳定性权衡双网同时在线功耗会比单网高这是物理规律没法完全消除但可以优化。我的经验是非必要时段让辅助网络进入低功耗状态。比如设备在待机时只保持主网络业务唤醒时再拉起辅助网络。这个策略要结合具体业务节奏来定不能一刀切。稳定性方面双网场景下最容易出问题的是网络抖动时的切换逻辑。主网络短暂断开又恢复如果切换逻辑太激进可能频繁在两条链路间来回切业务侧感知就是连接时断时续。处理方式是给切换加一个迟滞时间网络断开后等几秒确认不是瞬时抖动再切恢复后也等几秒确认稳定再切回。这个时间窗口设多长取决于业务对中断的容忍度我一般从 3 秒起步调。实操心得调试双网稳定性时我习惯用脚本模拟网络抖动——定时 down 再 up 某个网口观察业务侧的表现和日志里的切换记录。这种压力测试能提前暴露很多边界问题比等现场出问题再排查划算得多。6. 一些不那么显然的经验补充6.1 关于网络评分的一个反直觉现象有次调试发现Wi-Fi 连上之后以太网的路由规则还在但流量就是不按规则走。查了半天发现是以太网被系统标记成了“不可上网”状态——因为它的连通性检测captive portal 检测没通过。Android 会对每个网络做连通性探测探测失败的网络会被标记为NET_CAPABILITY_VALIDATED缺失某些路由逻辑会因此跳过它。这个现象反直觉的地方在于网络物理上是通的ping 网关也通但系统认为它“不能上网”于是分流规则形同虚设。解决办法是确认该网络的连通性探测能正常通过或者在内网环境下调整探测目标地址让探测走内网可达的地址。这个点在纯内网部署的设备上特别常见因为默认探测目标是公网地址内网根本访问不到。6.2 双网口设备的 MAC 与 ARP 注意事项如果设备有两个物理以太网口两个口的 MAC 地址必须不同否则交换机会混乱。有些硬件方案两个口共用一个 MAC靠 VLAN 区分这种方案在双网共存场景下要额外配置 VLAN复杂度上升不少。选型阶段就要确认硬件是否支持两个独立 MAC能省掉后期大量适配工作。ARP 层面也有个细节两个网口在同一网段时ARP 表可能串。虽然双网共存通常要求两个网络在不同网段但调试阶段难免遇到同网段的情况这时候要确认 ARP 响应从正确的口出去否则会出现“请求从 A 口发、响应从 B 口回”的诡异现象。6.3 日志与调试手段的取舍MTK 平台的网络日志比较丰富但全开的话刷屏严重反而看不清关键信息。我的做法是按模块分级开日志ConnectivityService开 info 级别看网络建立和回收路由相关的问题单独用ip monitor实时观察路由表变化iptables 的匹配情况用计数器看iptables -t mangle -L -v -n哪个规则命中多少次一目了然。ip monitor这个工具值得单独提一句它能实时打印路由表和规则的变化调试动态配置逻辑时特别有用。配置代码执行时盯着它的输出能立刻看出规则加没加上、顺序对不对比事后ip rule show猜要高效得多。6.4 量产前的回归清单调试通了不等于能量产双网共存涉及系统底层回归测试要覆盖的场景比普通功能多。我整理了一份自己用的清单冷启动后双网是否都正常、单网断开重连后分流是否恢复、双网同时断开再恢复的顺序是否影响结果、长时间运行后路由规则是否有残留、低电量或待机唤醒后网络状态是否一致。这几项过一遍基本能覆盖大部分现场会遇到的情况。其中“长时间运行后规则残留”这项最容易被跳过但恰恰是现场问题的高发区。设备跑几天之后规则越积越多某天突然分流失效排查起来要从头查起。所以清理逻辑的测试一定要做长时运行验证不能只测短时间内的连接断开。7. 最后聊几句实际体会双网共存这个需求技术上的难点其实不在某一条命令或者某一个接口而在于对 Android 网络框架整体行为的理解。评分机制、路由表、应用绑定这三层是相互影响的只改一层往往按下葫芦浮起瓢。我见过不少项目在路由层折腾很久没搞定最后发现是网络评分导致连接根本没保持住方向从一开始就偏了。另一个体会是调试手段比配置本身更重要。dumpsys connectivity、ip rule show、ip monitor、tcpdump、iptables 计数器这几个工具用熟了大部分问题都能在几分钟内定位。反过来如果只靠改配置试错一个问题可能耗掉一整天。还有一点双网方案没有“标准答案”不同项目的网络拓扑、业务分流需求、硬件能力都不一样上面给的配置只能算一个起点。真正落地时要根据自己项目的实际情况调整分流依据按地址还是按应用、绑定粒度进程级还是 socket 级、切换策略激进还是保守。把这些选择背后的逻辑想清楚比照抄一套配置有价值得多。