Android 多網絡支持

Android多個網絡連接

Android 5.0 LOLLIPOP (API Level 21) 

高級連接

新增的多網絡功能允許應用查詢可用網絡提供的功能,例如它們是 WLAN 網絡、蜂窩網絡還是按流量計費網絡,或者它們是否提供特定網絡功能。然後應用可以請求連接並對連接丟失或其他網絡變化作出響應。

Android 5.0 提供了新的多網絡 API,允許您的應用動態掃描具有特定能力的可用網絡,並與它們建立連接。當您的應用需要 SUPL、彩信或運營商計費網絡等專業化網絡時,或者您想使用特定類型的傳輸協議發送數據時,就可以使用此功能。

通過以上的Android版本更新文檔可以看出,Android 在 5.0 以上的系統中支持了多個網絡連接的特性,這個特性讓我一下就聯想到iOS中的Wi-Fi助理。

Apple Wi-Fi 助理的工作原理

通過 Wi-Fi 助理,即使您的 Wi-Fi 連接信號差,您仍可保持與互聯網的連接。例如,如果您在使用 Safari 時因 Wi-Fi 連接信號差而出現網頁無法載入的情況,Wi-Fi 助理將激活並自動切換到蜂窩移動網絡,以便網頁繼續載入。您可以將 Wi-Fi 助理與大多數應用(例如,Safari、Apple Music、“郵件”、“地圖”等)配合使用。 
當 Wi-Fi 助理激活時,您將在設備的狀態欄中看到蜂窩移動數據圖標。
由於當您的 Wi-Fi 連接信號差時,您將通過蜂窩移動網絡保持與互聯網的連接,您可能會用掉更多蜂窩移動數據。對於大多數用戶,這應只比以往的用量高出很小的比率。如果您對您的數據用量有疑問,請了解有關管理蜂窩移動數據的更多信息,或者聯繫 Apple 支持

 

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() 方法。
當系統檢測到合適的網絡時,它會連接到該網絡並調用 onAvailable() 回調。您可以使用回調中的 Network 對象來獲取有關網絡的更多信息,或者引導通信使用所選網絡。

app都採用指定的網絡

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) {
        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 中的實現


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 以下也是可以實現的?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章