安卓平台多网卡并发探讨

先转一篇介绍安卓网络选择(connectivity service)的介绍性文章,对Network,POLICY_TRANSPORT,NetworkAgent有基础性的介绍,已经熟悉的可以略过。

https://source.android.com/docs/core/connect/network-selection?hl=zh-cn

安卓操作系统的用户因为大多同时具备两个网络,WLAN(WIFI)和蜂窝网络(MOBILE),Linux操作系统也对多网卡有很好的支持,可以通过路由表很方便的在多网卡间切换。

安卓并没有给开发者暴露路由表这些接口,说明Google并不愿意把操作路由表切换网卡这种事暴露给用户,甚至不愿意暴露给开发者;例如,消费者不愿意在家里看网络视频的时候,担心应用还会消耗手机里那张SIM卡的流量;再例如,开发者也不愿意在写一个游戏APP的时候去关心手机上有哪些网卡,应该怎么设置路由表确保能连上游戏的服务器。当然,事情也在变化,近几年对于某些场景的开发者,也会产生”高端“的诉求,例如各种游戏加速器,开发者是要关心客户端和服务器之间连接通路问题,因为不同通路对应不同时延,对应不同的游戏操作体验。

安卓引入了Active Network的概念,同一时刻Network可以有多个,Active的只能是一个,大部分应用就用Active的Network即可,路由表安卓也设置好了,确保default 路由跟Active Network是匹配的,承担这个工作的就是connectivity service,对应的API是ConnectivityManager,开发者可以按Java的开发者手册去使用HttpClient, Socket,同时可以通过ConnectivityManager的API获取当前使用的Network信息,例如网络类型、IP地址、网关信息、是否计费等信息,选择必要的展现给消费者,例如在下载文件前,提醒可能会消耗手机卡的流量,因为此时使用的Network是MOBILE。

安卓引入了打分的机制,便于在多个Network间确定哪个是Active的,对于蜂窝和WIFI,很明显WIFI的打分高于蜂窝,这是Android的默认机制控制的,消费者不能修改,这个机制确保了用户回到家,WIFI连接上后,就不用担心手机流量的问题了。在安卓平台的电视上,有线网络(TYPE_ETHERNET)打分比WIFI更高,当消费者用网线连接电视和墙上的千兆网络接口后,很有可能优酷视频自动把电影的分辨率从720P切换到了1080P上,因为目前即使WIFI6的路由器也无法提供比网线很稳定的网络。再举一个例子,对于安装了VPN软件的用户,VPN的打分就比上述MOBILE、WIFI、ETHERNET的更高,这个机制确保了手机上其他需要访问网络的APP流量能被VPN APP接管。

那么,到底有没有使用非Active Network的场景呢

答案肯定是有,因为笔者就碰到了,再因为安卓已经提供了相关的接口

现在,还是以正常的逻辑理一理,WIFI开启的场景下,有没有必要使用蜂窝

答案是有:

1、玩游戏场景,用蜂窝来加速;另外,有些APP提供了手机号一键登录功能,这个也是要求必须用蜂窝网卡;

2、隐含的场景:是运营商VOLTE通话,在家上网的同时,能接打VOLTE通话,是因为ims网卡同时在工作,VOLTE通话都是走的蜂窝流量;

3、笔者的场景:手机连接一个WIFI局域网(无公网),访问内部主机的同时还需要访问一个位于公网的云服务。

场景价值理清楚后,接着说实现层面,在安卓平台,Google提供了requestNetwork接口,用于获取非Active Network,当应用收到onAvailable回调后,就可以访问指定的Network。

贴一段典型调用代码:

    public void requestMobileNetwork() {
        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkRequest.Builder builder = new NetworkRequest.Builder();
        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
        builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
        NetworkRequest build = builder.build();
        AppLog.i(TAG, "---> start request cell network");

        connectivityManager.requestNetwork(build, new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                super.onAvailable(network);
                
//create a socket bind to specify network
Socket socket = SocketFactory.getDefault().createSocket();
network.bindSocket(socket );
InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST_ADDR, HOST_PORT);

        socket.connect(inetSocketAddress, 5000);
//add some code here
            }
        });
    }

从上述代码中,network.bindSocket(socket );这行代表了把socket和指定的network绑定。

如果是使用URLConnection这种更偏应用场景的类,可以用如下接口:

Url url = new URL("http://xxxx.cn") ;
HttpURLConnection conn = (HttpURLConnection) network.openConnection(url);
conn.setConnectTimeout(3000);
conn.setRequestMethod("GET");
conn.getResponseCode();

如果是使用webview这种封装更好的类,可以使用上述接口将HttpURLConnection 和network绑定,再参考下述文章,构造一个WebResourceResponse,把网页内容填充到webview中

https://stackoverflow.com/questions/51775116/unable-to-set-httpurlconnection-headers-in-android-webview

再深入一点,探讨同一个应用如何同时使用两个Network。因为上述接口只是解决了切换问题,并存应该如何解决?答案应该很容易想到,同时request两个Network,分别绑定不同的socket就可以了。熟悉网络开发的同学应该得心应手,多线程管理不同的socket,每个socket再绑定不同的network。

笔者的场景就是如此,内外的主机通过WIFI与Android设备交互,Android设备通过运营商蜂窝网络与公网另一个主机连接,Android在两个网卡之间转发数据,扮演了路由器的角色。

最后,提下Android提供的维测接口

adb shell dumpsys connectivity

这个维测命令只需要连接adb端口,不需要root权限即可运行,打印的结果(作了匿名化处理)如下,包含几部分:

Active default network: 116 --当前激活网络

Current Networks: --当前所有网络列表

Requests: --按网卡统计的所有的网络请求

mNetworkRequestInfoLogs:--详细的请求Log

mNetworkInfoBlockingLogs: --被拒绝的请求Log

安卓在网络发生变化时,例如wifi断开后,遵循这样的规则,如果应用指定了transport类型是WIFI,那么就把这个应用的request挪到request大表中,不跟具体的wifi Network绑定,等wifi恢复后,再把应用的request挪回到wifi network的request表中,应用可以继续使用wifi网络,而不用担心wifi断开而蜂窝连接上的状态下,应用偷跑蜂窝流量。

假如应用不指定Network,直接创建socket,Android会用默认的Network提供网络连接,如果再使用socket过程中,网络发生切换,例如wifi断开,蜂窝连接成功,应用不做处理的话,安卓在底层(netd)会重新创建socket,应用感知不到socket层面的变化。如果应用要求感知这种切换,安卓提供了connectivityManager.registerNetworkCallback注册回调监听变化的接口,示例如下:

public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest networkRequest,
         NetworkCallback networkCallback) {
         if (connectivityManager == null || networkRequest == null || networkCallback == null) {
             Log.e(TAG, "connectivityManager networkRequest or networkCallback is null");
             return;
         }
         try {
             connectivityManager.registerNetworkCallback(networkRequest, networkCallback);
         //} catch (ConnectivityManager.TooManyRequestsException e) {
         //    Log.e(TAG, "catch too many requests exception, which is excepted.");
         } catch (RuntimeException e) {
             Log.e(TAG, "registerNetworkCallback occureed RuntimeException.");
         } catch (Exception e) {
             Log.e(TAG, "registerNetworkCallback occureed exception.");
         }
     }

private NetworkCallback mNetworkCallback = new NetworkCallback() {
         @Override
         public void onAvailable(Network network) {
             if (mUpdater != null) {
                 mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
             }
         }
 
         @Override
         public void onLost(Network network) {
             if (mUpdater != null) {
                 mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
             }
         }
     };

有了上述监听,应用可以在网络断开,网络恢复时增加处理代码,或者给消费者UI层面的展现。

总结:安卓在Linux基础上,对多网卡并发做了兼容处理,提供了更容易上手的API接口,方便开发者使用,也让消费者使用手机、平板、电视获得更好的用户体验。

你可能感兴趣的:(网络,android)