Android多网络连接探索-双Wifi

摘要:

新增的多网络功能允许应用查询可用网络提供的功能,例如它们是 WLAN 网络、蜂窝网络还是按流量计费网络,或者它们是否提供特定网络功能。然后应用可以请求连接并对连接丢失或其他网络变化作出响应。 Android 5.0 提供了新的多网络 API,允许您的应用动态扫描具有特定能力的可用网络,并与它们建立连接。

Android 5.0 LOLLIPOP (API Level 21)

新增的多网络功能允许应用查询可用网络提供的功能,例如它们是 WLAN 网络、蜂窝网络还是按流量计费网络,或者它们是否提供特定网络功能。然后应用可以请求连接并对连接丢失或其他网络变化作出响应。

Android 5.0 提供了新的多网络 API,允许您的应用动态扫描具有特定能力的可用网络,并与它们建立连接。当您的应用需要 SUPL、彩信或运营商计费网络等专业化网络时,或者您想使用特定类型的传输协议发送数据时,就可以使用此功能。

通过以上的Android版本更新文档可以看出,Android 在 5.0 以上的系统中支持了多个网络连接的特性。

Android 提供的这个特性意味着应用可以选择特定的网络发送网络数据。在用手机上网的时候很可能会遇到这种情况,已经连上了WiFi但是WiFi信号弱或者是该WiFi设备并没有连接到互联网,因此导致网络访问非常的缓慢甚至无法访问网络。但是这个时候手机的移动网络信号可能是非常好的,那么如果是在 Android 5.0 以下的系统上,我们只能关闭手机的WiFi功能,然后使用移动网络重新访问。在 Android 5.0 及以上的系统中有了这个特性之后,意味着应用可以自己处理好这种情况,直接切换到移动网络上面访问,为用户提供更好的体验。话不多说让我们来看一下怎么使用吧

setProcessDefaultNetwork

要从您的应用以动态方式选择并连接网络,请执行以下步骤:

1.创建一个 ConnectivityManager。
2.使用 NetworkRequest.Builder 类创建一个 NetworkRequest 对象,并指定您的应用感兴趣的网络功能和传输类型。
3.要扫描合适的网络,请调用 requestNetwork() 或 registerNetworkCallback(),并传入 NetworkRequest 对象和 ConnectivityManager.NetworkCallback 的实现。如果您想在检测到合适的网络时主动切换到该网络,请使用 requestNetwork() 方法;如果只是接收已扫描网络的通知而不需要主动切换,请改用 registerNetworkCallback() 方法。
4.当系统检测到合适的网络时,它会连接到该网络并调用 onAvailable() 回调。您可以使用回调中的 Network 对象来获取有关网络的更多信息,或者引导通信使用所选网络。

app都采用指定的网络

ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder req = newNetworkRequest.Builder();
req.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
cm.requestNetwork(req.build(), new ConnectivityManager.NetworkCallback() { @Override
public void onAvailable(Network network) {
 try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
ConnectivityManager.setProcessDefaultNetwork(network);
} else { 
connectivityManager.bindProcessToNetwork(network); 
}
} catch (IllegalStateException e) {
Log.e(TAG, "ConnectivityManager.NetworkCallback.onAvailable: ", e);       
 }    
}    
 // Be sure to override other options in NetworkCallback() too...}复制代码

指定某个请求采用指定的网络

ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);                       
NetworkRequest.Builder req = new NetworkRequest.Builder();
req.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
cm.requestNetwork(req.build(), new ConnectivityManager.NetworkCallback() {     

@Override    
public void onAvailable(Network network) {       
 // If you want to use a raw socket...        
network.bindSocket(...);       
 // Or if you want a managed URL connection...        
URLConnection conn = network.openConnection(new URL("http://www.baidu.com/"));   
 }     
// Be sure to override other options in NetworkCallback() too...}

android支持多种网络类型(WAN口),例如WIFI、3G等。目前android的实现是,WIFI和3G只能同时存在一个(优先级),例如当WIFI连接后,数据通路就从3G切换到WIFI。对上层app而言,这时候数据通路也就从3G切换到WIFI上。

考虑一个特殊的需求,某app只能通过WIFI接口去传输数据,是否可以实现?较新版本的android已经支持了该功能,通过调用setProcessDefaultNetwork()可以指定某一进程的网络接口,

    /**
 * Binds the current process to {@code network}.  All Sockets created in the future
 * (and not explicitly bound via a bound SocketFactory from
 * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
 * {@code network}.  All host name resolutions will be limited to {@code network} as well.
 * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
 * work and all host name resolutions will fail.  This is by design so an application doesn't
 * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
 * To clear binding pass {@code null} for {@code network}.  Using individually bound
 * Sockets created by Network.getSocketFactory().createSocket() and
 * performing network-specific host name resolutions via
 * {@link Network#getAllByName Network.getAllByName} is preferred to calling
 * {@code setProcessDefaultNetwork}.
 *
 * @param network The {@link Network} to bind the current process to, or {@code null} to clear
 *                the current binding.
 * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
 */
public static boolean setProcessDefaultNetwork(Network network) {

}

该函数的实现原理大致为,

  1. 该进程在创建socket时(app首先调用setProcessDefaultNetwork()),android底层会利用setsockopt函数设置该socket的SO_MARK为netId(android有自己的管理逻辑,每个Network有对应的ID),以后利用该socket发送的数据都会被打上netId的标记(fwmark 值)。
  2. 利用策略路由,将打着netId标记的数据包都路由到WIFI的接口wlan0。
    这里先介绍打标签的原理,至于策略路由的创建,后续再分析,下面是策略路由表的一个简单例子。
    shell@msm8916_64:/ $ ip rule list
    ip rule list
    0:      from all lookup local
    10000:  from all fwmark 0xc0000/0xd0000 lookup 99
    13000:  from all fwmark 0x10063/0x1ffff lookup 97
    13000:  from all fwmark 0x10064/0x1ffff lookup 1012
    14000:  from all oif rmnet_data0 lookup 1012
    15000:  from all fwmark 0x0/0x10000 lookup 99
    16000:  from all fwmark 0x0/0x10000 lookup 98
    17000:  from all fwmark 0x0/0x10000 lookup 97
    19000:  from all fwmark 0x64/0x1ffff lookup 1012
    22000:  from all fwmark 0x0/0xffff lookup 1012
    23000:  from all fwmark 0x0/0xffff uidrange 0-0 lookup main
    32000:  from all unreachable
    shell@msm8916_64:/ $

Android 中的实现

1. 先看一下 frameworks/base/core/java/android/net/ConnectivityManager.java 中 setProcessDefaultNetwork 的实现

public static boolean setProcessDefaultNetwork(Network network) {    int netId = (network == null) ? NETID_UNSET : network.netId;    if (netId == NetworkUtils.getBoundNetworkForProcess()) {        return true;    }    if (NetworkUtils.bindProcessToNetwork(netId)) {        // Set HTTP proxy system properties to match network.        // TODO: Deprecate this static method and replace it with a non-static version.        try {            Proxy.setHttpProxySystemProperty(getInstance().getDefaultProxy());        } catch (SecurityException e) {            // The process doesn't have ACCESS_NETWORK_STATE, so we can't fetch the proxy.            Log.e(TAG, "Can't set proxy properties", e);        }        // Must flush DNS cache as new network may have different DNS resolutions.        InetAddress.clearDnsCache();        // Must flush socket pool as idle sockets will be bound to previous network and may        // cause subsequent fetches to be performed on old network.        NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged();        return true;    } else {        return false;    }}复制代码

2. 在 setProcessDefaultNetwork 的时候,HttpProxy,DNS 都会使用当前网络的配置,再来看一下 NetworkUtils.bindProcessToNetwork
/frameworks/base/core/java/android/net/NetworkUtils.bindProcessToNetwork 其实是直接转到了 /system/netd/client/NetdClient.cpp 中

int setNetworkForTarget(unsigned netId, std::atomic_uint* target) {    if (netId == NETID_UNSET) {        *target = netId;        return 0;    }    // Verify that we are allowed to use |netId|, by creating a socket and trying to have it marked    // with the netId. Call libcSocket() directly; else the socket creation (via netdClientSocket())    // might itself cause another check with the fwmark server, which would be wasteful.    int socketFd;    if (libcSocket) {        socketFd = libcSocket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);    } else {        socketFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);    }    if (socketFd < 0) {        return -errno;    }    int error = setNetworkForSocket(netId, socketFd);    if (!error) {        *target = netId;    }    close(socketFd);    return error;} extern "C" int setNetworkForSocket(unsigned netId, int socketFd) {    if (socketFd < 0) {        return -EBADF;    }    FwmarkCommand command = {FwmarkCommand::SELECT_NETWORK, netId, 0};    return FwmarkClient().send(&command, socketFd);} extern "C" int setNetworkForProcess(unsigned netId) {    return setNetworkForTarget(netId, &netIdForProcess);}复制代码

3. 客户端发送 FwmarkCommand::SELECT_NETWORK 通知服务端处理,代码在 /system/netd/server/FwmarkServer.cpp

int FwmarkServer::processClient(SocketClient* client, int* socketFd) {   // .................   Fwmark fwmark;   socklen_t fwmarkLen = sizeof(fwmark.intValue);   if (getsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue, &fwmarkLen) == -1) {        return -errno;    }     switch (command.cmdId) {        // .................        case FwmarkCommand::SELECT_NETWORK: {            fwmark.netId = command.netId;            if (command.netId == NETID_UNSET) {                fwmark.explicitlySelected = false;                fwmark.protectedFromVpn = false;                permission = PERMISSION_NONE;            } else {                if (int ret = mNetworkController->checkUserNetworkAccess(client->getUid(),                                                                         command.netId)) {                    return ret;                }                fwmark.explicitlySelected = true;                fwmark.protectedFromVpn = mNetworkController->canProtect(client->getUid());            }            break;        }        // .................    }     fwmark.permission = permission;     if (setsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue,                   sizeof(fwmark.intValue)) == -1) {        return -errno;    }     return 0;}   union Fwmark {    uint32_t intValue;    struct {        unsigned netId          : 16;        bool explicitlySelected :  1;        bool protectedFromVpn   :  1;        Permission permission   :  2;    };    Fwmark() : intValue(0) {}};复制代码

最后其实只是给 socketFd 设置了 mark,为什么这样就可以达到使用特定网络的目的呢。这里的实现原理大致为:
1. 该进程在创建socket时(app首先调用setProcessDefaultNetwork()),android底层会利用setsockopt函数设置该socket的SO_MARK为netId(android有自己的管理逻辑,每个Network有对应的ID),以后利用该socket发送的数据都会被打上netId的标记(fwmark 值)。
2. 利用策略路由,将打着netId标记的数据包都路由到指定的网络接口,例如WIFI的接口wlan0。
Linux 中的策略路由暂不在本章展开讨论,这里只需要了解通过这种方式就能达到我们的目的。

Hook socket api

也就是说只要在当前进程中利用setsockopt函数设置所有socket的SO_MARK为netId,就可以完成所有的请求都走特定的网络接口。

1. 先来看一下 /bionic/libc/bionic/socket.cpp

int socket(int domain, int type, int protocol) {    return __netdClientDispatch.socket(domain, type, protocol);}复制代码

2. /bionic/libc/private/NetdClientDispatch.h

struct NetdClientDispatch {    int (*accept4)(int, struct sockaddr*, socklen_t*, int);    int (*connect)(int, const struct sockaddr*, socklen_t);    int (*socket)(int, int, int);    unsigned (*netIdForResolv)(unsigned);}; extern __LIBC_HIDDEN__ struct NetdClientDispatch __netdClientDispatch;复制代码

3. /bionic/libc/bionic/NetdClientDispatch.cpp

extern "C" __socketcall int __accept4(int, sockaddr*, socklen_t*, int);extern "C" __socketcall int __connect(int, const sockaddr*, socklen_t);extern "C" __socketcall int __socket(int, int, int); static unsigned fallBackNetIdForResolv(unsigned netId) {    return netId;} // This structure is modified only at startup (when libc.so is loaded) and never// afterwards, so it's okay that it's read later at runtime without a lock.__LIBC_HIDDEN__ NetdClientDispatch __netdClientDispatch __attribute__((aligned(32))) = {    __accept4,    __connect,    __socket,    fallBackNetIdForResolv,};复制代码

4. /bionic/libc/bionic/NetdClient.cpp

template <typename FunctionType>static void netdClientInitFunction(void* handle, const char* symbol, FunctionType* function) {    typedef void (*InitFunctionType)(FunctionType*);    InitFunctionType initFunction = reinterpret_cast<InitFunctionType>(dlsym(handle, symbol));    if (initFunction != NULL) {        initFunction(function);    }} static void netdClientInitImpl() {    void* netdClientHandle = dlopen("libnetd_client.so", RTLD_NOW);    if (netdClientHandle == NULL) {        // If the library is not available, it's not an error. We'll just use        // default implementations of functions that it would've overridden.        return;    }    netdClientInitFunction(netdClientHandle, "netdClientInitAccept4",                           &__netdClientDispatch.accept4);    netdClientInitFunction(netdClientHandle, "netdClientInitConnect",                           &__netdClientDispatch.connect);    netdClientInitFunction(netdClientHandle, "netdClientInitNetIdForResolv",                           &__netdClientDispatch.netIdForResolv);    netdClientInitFunction(netdClientHandle, "netdClientInitSocket", &__netdClientDispatch.socket);}static pthread_once_t netdClientInitOnce = PTHREAD_ONCE_INIT;extern "C" __LIBC_HIDDEN__ void netdClientInit() {    if (pthread_once(&netdClientInitOnce, netdClientInitImpl)) {        __libc_format_log(ANDROID_LOG_ERROR, "netdClient", "Failed to initialize netd_client");    }}复制代码

5. /system/netd/client/NetdClient.cpp

extern "C" void netdClientInitSocket(SocketFunctionType* function) {    if (function && *function) {        libcSocket = *function;        *function = netdClientSocket;    }} int netdClientSocket(int domain, int type, int protocol) {    int socketFd = libcSocket(domain, type, protocol);    if (socketFd == -1) {        return -1;    }    unsigned netId = netIdForProcess;    if (netId != NETID_UNSET && FwmarkClient::shouldSetFwmark(domain)) {        if (int error = setNetworkForSocket(netId, socketFd)) {            return closeFdAndSetErrno(socketFd, error);        }    }    return socketFd;}复制代码

int netdClientAccept4(int sockfd, sockaddr* addr, socklen_t* addrlen, int flags);
int netdClientConnect(int sockfd, const sockaddr* addr, socklen_t addrlen);
int netdClientSocket(int domain, int type, int protocol);

看到这里应该明白了,以上的函数和 libc 中的 accpet / connect / socket 功能相同,只是额外的将 socket 的SO_MARK设为netId。注意:netIdForProcess 为之前调用 setProcessDefaultNetwork 时保存下来的值。

所以当调用 libc 中的 connect() 的时候, connect() -> netdClientConnect() -> __connect(),也就完成了将所有 socket 的SO_MARK设置为netId了。

自然在应用中无论是通过 Java 新建的网络连接,还是通过 native 代码新建的网络连接,只要最后是通过 libc 中的接口就能使用该功能。至于连着WiFi最后流量耗了一大堆的问题,可能会让用户再次陷入是否应该关闭iOS 11中WiFi助理功能类似的纠结。无论如何从技术上来讲这是一个优化点,说来 Linux 本身是支持的,也许在 Android 5.0 以下也是可以实现的?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342