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:
kotlinfun 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刚好提供了一个这样的接口:
arduinoint android_setsocknetwork(net_handle_t network, int fd)
该接口的第一个参数就是Network中的handle字段,因此直接通过public long getNetworkHandle();将该字段从java层传下来即可。
上述两种方案都试了一下,测试ok。比较坑的是注意socketfd回调出来的时机: 必须在socket connect之前,不然绑定是不会生效的。