ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MTK Android 9.0双网共存实战:路由分流与应用绑定详解

MTK Android 9.0双网共存实战:路由分流与应用绑定详解

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 的网络栈底层还是 Linux,ip 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 各自有评分逻辑,以太网通常评分较高(比如 70),Wi-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 环境确认与前置检查

动手之前先把现状摸清楚,这一步能省掉后面大量返工。我习惯按下面的顺序过一遍:

  1. 确认两个网络接口都能独立工作。先单独插网线测以太网,再单独连 Wi-Fi 测无线,确保各自单独使用时都正常。如果单独用都有问题,双网肯定更不行。
  2. 确认系统版本和网络框架版本。getprop ro.build.version.release确认是 9.0,再看一下ConnectivityService相关代码分支,确认没有特殊的定制逻辑。
  3. 确认 root 权限可用。改路由表、加 iptables 规则都需要 root,调试阶段用 adb root 即可,量产方案要把这些配置固化到系统启动流程里。
  4. 记录两个网络的网段和网关。比如以太网 192.168.1.0/24 网关 192.168.1.1,Wi-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 相关。假设以太网对应表 1001,Wi-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 应用绑定后仍然走默认网络

这个问题的排查我有个固定套路,按顺序过:

  1. 确认绑定的 Network 对象当前是 available 状态。用getNetworkCapabilities查一下,返回 null 说明对象已失效。
  2. 确认绑定调用的时机。在网络还没建立时绑定,绑定会失败且不报错,静默失效。
  3. 确认 socket 的创建方式。如果 socket 是在绑定之前创建的,绑定对已存在的 socket 不生效,只对绑定之后新建的 socket 生效。
  4. 确认没有其他代码覆盖绑定。进程里如果有多处调用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 级)、切换策略(激进还是保守)。把这些选择背后的逻辑想清楚,比照抄一套配置有价值得多。

返回列表