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 助理与大多数应用 (例如, SafariApple Music 邮件地图等) 配合使用
当 Wi-Fi 助理激活时, 您将在设备的状态栏中看到蜂窝移动数据图标
由于当您的 Wi-Fi 连接信号差时, 您将通过蜂窝移动网络保持与互联网的连接, 您可能会用掉更多蜂窝移动数据对于大多数用户, 这应只比以往的用量高出很小的比率如果您对您的数据用量有疑问, 请了解有关管理蜂窝移动数据的更多信息, 或者联系 Apple 支持
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) {}
- };
最后其实只是给 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 以下也是可以实现的?
来源: https://juejin.im/post/5aa87dcbf265da23783ff52e