Android多个网络连接

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

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 以下也是可以实现的?

目录
相关文章
|
2月前
|
Android开发
Android网络访问超时
Android网络访问超时
30 2
|
2月前
|
Java Linux API
统计android设备的网络数据使用量
统计android设备的网络数据使用量
52 0
|
1月前
|
缓存 网络协议 安全
Android网络面试题之Http基础和Http1.0的特点
**HTTP基础:GET和POST关键差异在于参数传递方式(GET在URL,POST在请求体),安全性(POST更安全),数据大小限制(POST无限制,GET有限制),速度(GET较快)及用途(GET用于获取,POST用于提交)。面试中常强调POST的安全性、数据量、数据类型支持及速度。HTTP 1.0引入了POST和HEAD方法,支持多种数据格式和缓存,但每个请求需新建TCP连接。**
30 5
|
1月前
|
安全 网络协议 算法
Android网络基础面试题之HTTPS的工作流程和原理
HTTPS简述 HTTPS基于TCP 443端口,通过CA证书确保服务器身份,使用DH算法协商对称密钥进行加密通信。流程包括TCP握手、证书验证(公钥解密,哈希对比)和数据加密传输(随机数加密,预主密钥,对称加密)。特点是安全但慢,易受特定攻击,且依赖可信的CA。每次请求可能复用Session ID以减少握手。
28 2
|
1月前
|
缓存 JSON 网络协议
Android面试题:App性能优化之电量优化和网络优化
这篇文章讨论了Android应用的电量和网络优化。电量优化涉及Doze和Standby模式,其中应用可能需要通过用户白名单或电池广播来适应限制。Battery Historian和Android Studio的Energy Profile是电量分析工具。建议减少不必要的操作,延迟非关键任务,合并网络请求。网络优化包括HTTPDNS减少DNS解析延迟,Keep-Alive复用连接,HTTP/2实现多路复用,以及使用protobuf和gzip压缩数据。其他策略如使用WebP图像格式,按网络质量提供不同分辨率的图片,以及启用HTTP缓存也是有效手段。
47 9
|
1月前
|
缓存 网络协议 Android开发
Android网络面试题之Http1.1和Http2.0
HTTP/1.1 引入持久连接和管道机制提升效率,支持分块传输编码和更多请求方式如PUT、PATCH。Host字段指定服务器域名,RANGE用于断点续传。HTTP/2变为二进制协议,实现多工处理,头信息压缩和服务器推送,减少延迟并优化资源加载。HTTP不断发展,从早期的简单传输到后来的高效交互。
26 0
Android网络面试题之Http1.1和Http2.0
|
1月前
|
JSON Java API
【Android】使用 Retrofit2 发送异步网络请求的简单案例
**摘要:** Retrofit是Android和Java的HTTP客户端库,简化了RESTful API交互。它通过Java接口定义HTTP请求,并提供注解管理参数、HTTP方法等。要使用Retrofit,首先在AndroidManifest.xml中添加`INTERNET`权限,然后在`build.gradle`中引入Retrofit和Gson依赖。创建服务器响应数据类和描述接口的接口,如`Result`和`Api`。通过Retrofit.Builder配置基础URL并构建实例,之后调用接口方法创建Call对象并发送异步请求。
61 1
|
1月前
|
缓存 网络协议 Java
Android面试题之Java网络通信基础知识
Socket是应用与TCP/IP通信的接口,封装了底层细节。网络通信涉及连接、读写数据。BIO是同步阻塞,NIO支持多路复用(如Selector),AIO在某些平台提供异步非阻塞服务。BIO示例中,服务端用固定线程池处理客户端请求,客户端发起连接并读写数据。NIO的关键是Selector监控多个通道的事件,减少线程消耗。书中推荐《Java网络编程》和《UNIX网络编程》。关注公众号AntDream了解更多。
26 2
|
1月前
|
XML JSON Java
Android面试题 之 网络通信基础面试题
序列化对比:Serializable码流大、性能低;XML人机可读但复杂;JSON轻量、兼容性好但空间消耗大;ProtoBuff高效紧凑。支持大量长连接涉及系统限制调整、缓冲区优化。select/poll/epoll是IO多路复用,epoll在高连接数下性能更优且支持边缘触发。水平触发持续通知数据,边缘触发仅通知新数据。直接内存减少一次拷贝,零拷贝技术如sendfile和MMAP提升效率。关注公众号&quot;AntDream&quot;了解更多技术细节。
20 1
|
1月前
|
安全 Android开发 数据安全/隐私保护
同样的 APP 为何在 Android 8 以后网络感觉变卡?
【6月更文挑战第8天】Android 8 及以后系统中,APP 网络感觉变卡源于更严格的安全机制和后台限制,系统对网络优化的侧重改变,以及APP自身兼容性问题。开发者需优化APP,适应新系统,用户可更新APP或检查权限设置。通过共同努力,有望改善网络卡顿现象,提升用户体验。