摘要:
新增的多網(wǎng)絡(luò)功能允許應(yīng)用查詢可用網(wǎng)絡(luò)提供的功能镶殷,例如它們是 WLAN 網(wǎng)絡(luò)晦毙、蜂窩網(wǎng)絡(luò)還是按流量計(jì)費(fèi)網(wǎng)絡(luò)恋拷,或者它們是否提供特定網(wǎng)絡(luò)功能准给。然后應(yīng)用可以請求連接并對連接丟失或其他網(wǎng)絡(luò)變化作出響應(yīng)响巢。 Android 5.0 提供了新的多網(wǎng)絡(luò) API描滔,允許您的應(yīng)用動態(tài)掃描具有特定能力的可用網(wǎng)絡(luò),并與它們建立連接踪古。
Android 5.0 LOLLIPOP (API Level 21)
新增的多網(wǎng)絡(luò)功能允許應(yīng)用查詢可用網(wǎng)絡(luò)提供的功能含长,例如它們是 WLAN 網(wǎng)絡(luò)、蜂窩網(wǎng)絡(luò)還是按流量計(jì)費(fèi)網(wǎng)絡(luò)伏穆,或者它們是否提供特定網(wǎng)絡(luò)功能拘泞。然后應(yīng)用可以請求連接并對連接丟失或其他網(wǎng)絡(luò)變化作出響應(yīng)。
Android 5.0 提供了新的多網(wǎng)絡(luò) API枕扫,允許您的應(yīng)用動態(tài)掃描具有特定能力的可用網(wǎng)絡(luò)陪腌,并與它們建立連接。當(dāng)您的應(yīng)用需要 SUPL烟瞧、彩信或運(yùn)營商計(jì)費(fèi)網(wǎng)絡(luò)等專業(yè)化網(wǎng)絡(luò)時(shí)诗鸭,或者您想使用特定類型的傳輸協(xié)議發(fā)送數(shù)據(jù)時(shí),就可以使用此功能参滴。
通過以上的Android版本更新文檔可以看出强岸,Android 在 5.0 以上的系統(tǒng)中支持了多個網(wǎng)絡(luò)連接的特性。
Android 提供的這個特性意味著應(yīng)用可以選擇特定的網(wǎng)絡(luò)發(fā)送網(wǎng)絡(luò)數(shù)據(jù)砾赔。在用手機(jī)上網(wǎng)的時(shí)候很可能會遇到這種情況蝌箍,已經(jīng)連上了WiFi但是WiFi信號弱或者是該WiFi設(shè)備并沒有連接到互聯(lián)網(wǎng)青灼,因此導(dǎo)致網(wǎng)絡(luò)訪問非常的緩慢甚至無法訪問網(wǎng)絡(luò)。但是這個時(shí)候手機(jī)的移動網(wǎng)絡(luò)信號可能是非常好的十绑,那么如果是在 Android 5.0 以下的系統(tǒng)上聚至,我們只能關(guān)閉手機(jī)的WiFi功能,然后使用移動網(wǎng)絡(luò)重新訪問本橙。在 Android 5.0 及以上的系統(tǒng)中有了這個特性之后扳躬,意味著應(yīng)用可以自己處理好這種情況,直接切換到移動網(wǎng)絡(luò)上面訪問甚亭,為用戶提供更好的體驗(yàn)贷币。話不多說讓我們來看一下怎么使用吧
setProcessDefaultNetwork
要從您的應(yīng)用以動態(tài)方式選擇并連接網(wǎng)絡(luò),請執(zhí)行以下步驟:
1.創(chuàng)建一個 ConnectivityManager亏狰。
2.使用 NetworkRequest.Builder 類創(chuàng)建一個 NetworkRequest 對象役纹,并指定您的應(yīng)用感興趣的網(wǎng)絡(luò)功能和傳輸類型。
3.要掃描合適的網(wǎng)絡(luò)暇唾,請調(diào)用 requestNetwork() 或 registerNetworkCallback()促脉,并傳入 NetworkRequest 對象和 ConnectivityManager.NetworkCallback 的實(shí)現(xiàn)。如果您想在檢測到合適的網(wǎng)絡(luò)時(shí)主動切換到該網(wǎng)絡(luò)策州,請使用 requestNetwork() 方法瘸味;如果只是接收已掃描網(wǎng)絡(luò)的通知而不需要主動切換,請改用 registerNetworkCallback() 方法够挂。
4.當(dāng)系統(tǒng)檢測到合適的網(wǎng)絡(luò)時(shí)旁仿,它會連接到該網(wǎng)絡(luò)并調(diào)用 onAvailable() 回調(diào)。您可以使用回調(diào)中的 Network 對象來獲取有關(guān)網(wǎng)絡(luò)的更多信息孽糖,或者引導(dǎo)通信使用所選網(wǎng)絡(luò)枯冈。
app都采用指定的網(wǎng)絡(luò)
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...}復(fù)制代碼
指定某個請求采用指定的網(wǎng)絡(luò)
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支持多種網(wǎng)絡(luò)類型(WAN口),例如WIFI办悟、3G等尘奏。目前android的實(shí)現(xiàn)是,WIFI和3G只能同時(shí)存在一個(優(yōu)先級)病蛉,例如當(dāng)WIFI連接后炫加,數(shù)據(jù)通路就從3G切換到WIFI。對上層app而言铡恕,這時(shí)候數(shù)據(jù)通路也就從3G切換到WIFI上琢感。
考慮一個特殊的需求丢间,某app只能通過WIFI接口去傳輸數(shù)據(jù)探熔,是否可以實(shí)現(xiàn)?較新版本的android已經(jīng)支持了該功能烘挫,通過調(diào)用setProcessDefaultNetwork()可以指定某一進(jìn)程的網(wǎng)絡(luò)接口诀艰,
/**
* 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) {
}
該函數(shù)的實(shí)現(xiàn)原理大致為柬甥,
- 該進(jìn)程在創(chuàng)建socket時(shí)(app首先調(diào)用setProcessDefaultNetwork()),android底層會利用setsockopt函數(shù)設(shè)置該socket的SO_MARK為netId(android有自己的管理邏輯其垄,每個Network有對應(yīng)的ID)苛蒲,以后利用該socket發(fā)送的數(shù)據(jù)都會被打上netId的標(biāo)記(fwmark 值)。
- 利用策略路由绿满,將打著netId標(biāo)記的數(shù)據(jù)包都路由到WIFI的接口wlan0臂外。
這里先介紹打標(biāo)簽的原理,至于策略路由的創(chuàng)建喇颁,后續(xù)再分析漏健,下面是策略路由表的一個簡單例子。
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 中的實(shí)現(xiàn)
1. 先看一下 frameworks/base/core/java/android/net/ConnectivityManager.java 中 setProcessDefaultNetwork 的實(shí)現(xiàn)
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; }}復(fù)制代碼
2. 在 setProcessDefaultNetwork 的時(shí)候橘霎,HttpProxy蔫浆,DNS 都會使用當(dāng)前網(wǎng)絡(luò)的配置,再來看一下 NetworkUtils.bindProcessToNetwork
/frameworks/base/core/java/android/net/NetworkUtils.bindProcessToNetwork 其實(shí)是直接轉(zhuǎn)到了 /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);}復(fù)制代碼
3. 客戶端發(fā)送 FwmarkCommand::SELECT_NETWORK 通知服務(wù)端處理姐叁,代碼在 /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) {}};復(fù)制代碼
最后其實(shí)只是給 socketFd 設(shè)置了 mark瓦盛,為什么這樣就可以達(dá)到使用特定網(wǎng)絡(luò)的目的呢。這里的實(shí)現(xiàn)原理大致為:
1. 該進(jìn)程在創(chuàng)建socket時(shí)(app首先調(diào)用setProcessDefaultNetwork())外潜,android底層會利用setsockopt函數(shù)設(shè)置該socket的SO_MARK為netId(android有自己的管理邏輯原环,每個Network有對應(yīng)的ID),以后利用該socket發(fā)送的數(shù)據(jù)都會被打上netId的標(biāo)記(fwmark 值)橡卤。
2. 利用策略路由扮念,將打著netId標(biāo)記的數(shù)據(jù)包都路由到指定的網(wǎng)絡(luò)接口,例如WIFI的接口wlan0碧库。
Linux 中的策略路由暫不在本章展開討論柜与,這里只需要了解通過這種方式就能達(dá)到我們的目的。
Hook socket api
也就是說只要在當(dāng)前進(jìn)程中利用setsockopt函數(shù)設(shè)置所有socket的SO_MARK為netId嵌灰,就可以完成所有的請求都走特定的網(wǎng)絡(luò)接口弄匕。
1. 先來看一下 /bionic/libc/bionic/socket.cpp
int socket(int domain, int type, int protocol) { return __netdClientDispatch.socket(domain, type, protocol);}復(fù)制代碼
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;復(fù)制代碼
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,};復(fù)制代碼
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"); }}復(fù)制代碼
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;}復(fù)制代碼
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);
看到這里應(yīng)該明白了,以上的函數(shù)和 libc 中的 accpet / connect / socket 功能相同沽瞭,只是額外的將 socket 的SO_MARK設(shè)為netId迁匠。注意:netIdForProcess 為之前調(diào)用 setProcessDefaultNetwork 時(shí)保存下來的值。
所以當(dāng)調(diào)用 libc 中的 connect() 的時(shí)候, connect() -> netdClientConnect() -> __connect()驹溃,也就完成了將所有 socket 的SO_MARK設(shè)置為netId了城丧。
自然在應(yīng)用中無論是通過 Java 新建的網(wǎng)絡(luò)連接,還是通過 native 代碼新建的網(wǎng)絡(luò)連接豌鹤,只要最后是通過 libc 中的接口就能使用該功能亡哄。至于連著WiFi最后流量耗了一大堆的問題,可能會讓用戶再次陷入是否應(yīng)該關(guān)閉iOS 11中WiFi助理功能類似的糾結(jié)布疙。無論如何從技術(shù)上來講這是一個優(yōu)化點(diǎn)蚊惯,說來 Linux 本身是支持的愿卸,也許在 Android 5.0 以下也是可以實(shí)現(xiàn)的?