Android 5.0 LOLLIPOP (API Level 21)
新增的多網路功能允許應用查詢可用網路提供的功能,例如它們是 WLAN 網路、蜂窩網路還是按流量計費網路,或者它們是否提供特定網路功能。然後應用可以請求連線並對連線丟失或其他網路變化作出響應。
Android 5.0 提供了新的多網路 API,允許您的應用動態掃描具有特定能力的可用網路,並與它們建立連線。當您的應用需要 SUPL、彩信或運營商計費網路等專業化網路時,或者您想使用特定型別的傳輸協議傳送資料時,就可以使用此功能。
通過以上的Android版本更新文件可以看出,Android 在 5.0 以上的系統中支援了多個網路連線的特性,這個特性讓我一下就聯想到iOS中的Wi-Fi助理。
Apple Wi-Fi 助理的工作原理
Android 提供的這個特性意味著應用可以選擇特定的網路傳送網路資料。在用手機上網的時候很可能會遇到這種情況,已經連上了WiFi但是WiFi訊號弱或者是該WiFi裝置並沒有連線到網際網路,因此導致網路訪問非常的緩慢甚至無法訪問網路。但是這個時候手機的行動網路訊號可能是非常好的,那麼如果是在 Android 5.0 以下的系統上,我們只能關閉手機的WiFi功能,然後使用行動網路重新訪問。在 Android 5.0 及以上的系統中有了這個特性之後,意味著應用可以自己處理好這種情況,直接切換到行動網路上面訪問,為使用者提供更好的體驗。話不多說讓我們來看一下怎麼使用吧
setProcessDefaultNetwork
要從您的應用以動態方式選擇並連線網路,請執行以下步驟:
- 建立一個
ConnectivityManager
。 - 使用
NetworkRequest.Builder
類建立一個NetworkRequest
物件,並指定您的應用感興趣的網路功能和傳輸型別。 - 要掃描合適的網路,請呼叫
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) {}
};複製程式碼
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 以下也是可以實現的?
原文作者:冇閒