Android端WIFI/流量共存技术方案

1 需求背景

最近在做一个和IOT设备相关的App,App和设备会连接到IOT设备的热点Wifi上,但是该Wifi无法上网。同时App有和后端服务器进行交互的需求,因此又需要APP能够同时通过流量上网。

2 技术调研

下面这三个概念先熟悉一下, 后面的方案和这三个息息相关:

  • ConnectivityManager: Android提供的操作手机网络相关的系统服务,可以连接Wifi,查询手机的网络信息;
  • Network: 表示一种网络配置的基本信息单元,简单理解,WiFi是一个Network, 流量是另一个Network;
  • Socket: 这里既指Java层的Socket,也包括NDK中通过socket()创建的socketFD;

ConnectivityManager提供API查询当前手机连接有哪些Network,以及bindProcessToNetwork, bindSocket, createSocket等API来更精细化的控制网络通信:

  • bindProcessToNetwork: 将当前App进程绑定到某一个Network,即app中所有的网络通信都会通过这个Network进行处理;
  • bindSocket: 将当前进程创建的某一个socket绑定到某一个Network,即这个socket的通信流程通过这个Network处理,App中的其余网络通信还是通过系统默认Network处理;
  • createSocket: 创建一个和某个Network绑定的socket,这个socket的网络通信只能通过这个Network进行处理;

例如获取并绑定APP网络到流量:

kotlin 复制代码
 fun bindToCellular(context: Context, success: Consumer<Boolean>) {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val builder = NetworkRequest.Builder()
    builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
    val request = builder.build()
    val callback: ConnectivityManager.NetworkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            connectivityManager.bindProcessToNetwork(network)
            success.accept(true)
        }
    }
    connectivityManager.requestNetwork(request, callback)
} 

获取已连接的Wifi:

kotlin 复制代码
 fun getWifiNetwork(context: Context): Network? {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    for (network in connectivityManager.allNetworks) {
        val networkInfo = connectivityManager.getNetworkInfo(network)
        if (networkInfo != null && ConnectivityManager.TYPE_WIFI == networkInfo.type && networkInfo.isConnected) {
            return network
        }
    }
    return null
}

App在连接Wifi之后,一般都需要强制使用bindProcessToNetwork将App绑定到这个Wifi上,否则有的版本上Wifi会很快断开连接。因此我们的网络交互流程可能会变成下面这样子:

rust 复制代码
 扫描Wifi -> 连接Wifi -> 绑定Wifi -> 绑定流量。 

我们期望的是同时绑定了Wifi和流量之后,App能够自动根据我们的网络请求决定是走Wifi还是流量。但是现实却不是这样的。在Google的设计中,bindProcessToNetwork只能让App绑定在一个Network上,多次调用,会以最后绑定的为主:

  • 绑定Wifi -> 绑定流量, 只会走流量;
  • 绑定流量 -> 绑定Wifi, 只会走Wifi。

3 技术方案

如果只是一个纯Java层的App,不涉及到NDK中使用网络通信,那么直接采用如下方案应该可以解决问题:

绑定Wifi -> 绑定流量, 然后在Okhttp中添加一个自定义的SocketFactory,将SocketFactory创建的Socket和Wifi进行绑定,总之就是将创建的socket通过Network中的bindSocket和Wifi进行绑定即可。

比较复杂的是我的App中使用了大量的NDK的代码,在NDK中使用了一些三方库,例如FFMpeg。这些三方库直接通过socket(), connect()这些系统调用API来进行网络通信。

因此是不是也有办法将NDK中创建的socketfd和wifi network进行绑定成为了解决这个问题的核心。

第一步: 如何拿到这些fd? 对使用的三方库的api一顿搜索,有的暴露了socket创建的回调接口,有的完全就没有暴露(比如FFMpeg)。没有暴露接口的只能把源码拿下来,找到内部创建socket的地方,自己添加个回调接口进去把fd传出来。

第二步: 拿到fd之后,如何进行绑定。

  • 通过jni把fd传出去到java层,创建一个FileDescriptor(), FileDescriptor也可以和network进行绑定;

  • 是否可以直接在jni进行绑定: 查了一下,Android刚好提供了一个这样的接口:

    arduino 复制代码
    int android_setsocknetwork(net_handle_t network, int fd)

    该接口的第一个参数就是Network中的handle字段,因此直接通过public long getNetworkHandle();将该字段从java层传下来即可。

上述两种方案都试了一下,测试ok。比较坑的是注意socketfd回调出来的时机: 必须在socket connect之前,不然绑定是不会生效的。

相关推荐
每次的天空7 小时前
Android学习总结之算法篇四(字符串)
android·学习·算法
x-cmd8 小时前
[250331] Paozhu 发布 1.9.0:C++ Web 框架,比肩脚本语言 | DeaDBeeF 播放器发布 1.10.0
android·linux·开发语言·c++·web·音乐播放器·脚本语言
tangweiguo0305198711 小时前
Android BottomNavigationView 完全自定义指南:图标、文字颜色与选中状态
android
遥不可及zzz12 小时前
Android 应用程序包的 adb 命令
android·adb
无知的前端12 小时前
Flutter 一文精通Isolate,使用场景以及示例
android·flutter·性能优化
_一条咸鱼_12 小时前
Android Compose 入门之字符串与本地化深入剖析(五十三)
android
ModestCoder_13 小时前
将一个新的机器人模型导入最新版isaacLab进行训练(以unitree H1_2为例)
android·java·机器人
robin_suli14 小时前
Spring事务的传播机制
android·java·spring
鸿蒙布道师15 小时前
鸿蒙NEXT开发对象工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
Harrison_zhu16 小时前
Ubuntu18.04 编译 Android7.1代码报错
android