0%

Netty 在 Android 平台多网络环境指定网卡通信实践

Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

Android 平台,经常用 Netty 来对接物联网设备,处理各种自定义协议的场景。

这不今天碰到个场景,说需要终端在同时连接 蜂窝数据WiFi 的时候,应用程序能够一面使用 WiFi 网络和一些物联网设备通信,一面使用 蜂窝数据 和服务器通信。并且这两种访问是并行的,协同工作。

Android 的网络策略分析

针对 Android 的外观表现,在连接 WiFi 之后,信号棒旁边的数据就自动消失了。难道此时系统已经关闭了蜂窝数据?

为了验证这个猜想,直接进入 shell 查看这种清晰下网卡的状况:

garnet:/ $ ifconfig
lo        Link encap:UNSPEC
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope: Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:895824 errors:0 dropped:0 overruns:0 frame:0
          TX packets:895824 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:1948348327 TX bytes:1948348327

rmnet_data1 Link encap:UNSPEC
          inet addr:10.170.143.153  Mask:255.255.255.252
          UP RUNNING  MTU:1400  Metric:1
          RX packets:272007 errors:0 dropped:0 overruns:0 frame:0
          TX packets:247936 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:414916964 TX bytes:64001092

wlan0     Link encap:UNSPEC    Driver icnss2
          inet addr:192.168.8.180  Bcast:192.168.8.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:12390788 errors:0 dropped:1 overruns:0 frame:0
          TX packets:6105491 errors:0 dropped:4 overruns:0 carrier:0
          collisions:0 txqueuelen:3000
          RX bytes:13957403104 TX bytes:1393348123

可以看到同时存在两个网卡,一个是蜂窝数据,另一个是 WiFi。

那么刚刚的猜测就否定了,也说明 Android 系统不是靠禁用网卡来决定使用什么网络,猜测可能是用了路由控制等手段。

那这两个网卡现在都还能使用吗?继续使用命令验证一下:

# 验证蜂窝网络
garnet:/ $ curl --interface rmnet_data1 \
-H "accept:text/html" \
-H "user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0" \
http://2024.ip138.com/ | grep IP

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   917  100   917    0     0   2682      0 --:--:-- --:--:-- --:--:--  2689
<title>您的IP地址是:116.169.10.10</title>

# 验证 WiFi 网络
garnet:/ $ curl --interface wlan0 \
-H "accept:text/html" \
-H "user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0" \
http://2024.ip138.com/ | grep IP

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   919  100   919    0     0   7994      0 --:--:-- --:--:-- --:--:--  8061
<title>您的IP地址是:119.4.66.194</title>

这里用了 http://2024.ip138.com/ 来回显请求 IP,可以看到使用不同的网卡回显的 IP 是不一样的。

也证明此时两张网卡都是能用的。

Android 应用如何使用不同的网络

网卡都能用,那么应用有办法向上面命令行那样指定某个网卡发出出站请求吗?

仅仅使用 Java 原生的方案,或许可以但兼容性无法保证。

不如来看看 Android 官方给出的解决方案:
https://developer.android.com/reference/android/net/ConnectivityManager#requestNetwork(android.net.NetworkRequest,%20android.net.ConnectivityManager.NetworkCallback)

Android Framework 留了一个 public void requestNetwork (NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) 给应用,用于向系统申请某种类型的网络,就绪后在 NetworkCallback 回调回来一个 Network 类:
https://developer.android.com/reference/android/net/Network

这个类没有太多有价值的方法,其中有一个 public SocketFactory getSocketFactory () 方法,返回的 SocketFactory 能够和具体类型的网络关联。

换句话说,这个 NetworkSocketFactory 创建的 Socket 就能够实现从具体的网卡发送接收数据。

接下来就看看应用的支持情况。

应用支持情况

Java

Java 的各种类没啥好说的,肯定是兼容 SocketFactory 的。重点是看看常用的三方库的支持情况。

OkHttp

Android 应用开发里面用的比较多的 OkHttp 库,能够方便的设置自定义的 SocketFactory,经过测试可以实现从不同网卡收发数据的效果。

演示代码如下:

    private void okhttpCall() {
        ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
        connectivityManager.requestNetwork(
                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR).build(),
                new ConnectivityManager.NetworkCallback() {
                    @Override
                    public void onAvailable(@NonNull Network network) {
                        okhttpCallWebServer(network);
                    }
                }
        );
        connectivityManager.requestNetwork(
                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(),
                new ConnectivityManager.NetworkCallback() {
                    @Override
                    public void onAvailable(@NonNull Network network) {
                        okhttpCallWebServer(network);
                    }
                }
        );
    }

    private void okhttpCallWebServer(Network network) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .socketFactory(network.getSocketFactory())
                .build();
        okHttpClient
                .newCall(new Request.Builder()
                        .url("https://2024.ip138.com/")
                        .addHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0")
                        .build())
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {

                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        String result = response.body().string();
                        String[] lines = result.split("\\r?\\n");
                        for (String line : lines) {
                            if (line.contains("IP")) {
                                Log.w("RESULT", "okhttpCallWebServer:" + line);
                                break;
                            }
                        }
                    }
                });
    }

执行 okhttpCall 方法后控制台打印如下:

2024-03-26 22:56:12.104 26243-26307 RESULT com.liux.myapplication W  okhttpCallWebServer:<title>您的IP地址是:119.4.66.194</title>
2024-03-26 22:56:12.348 26243-26306 RESULT com.liux.myapplication W  okhttpCallWebServer:<title>您的IP地址是:116.169.10.10</title>

Netty

Java 生态中还有一个重量级选手 Netty,也能在 Android 发挥极大的作用。尤其是各种物联网的场景中。

Netty 应用 SocketFactory 就稍微麻烦一点点。

一般情况下,使用 Netty 的目的有两个:

  1. 借助 Netty 封装的 NIO,实现高性能的服务器。
  2. 借助 Netty 灵活的协议处理,方便实现各种 Socket 自定义协议封装、解析。

并且通常情况下都是这样用的:

    private void nettyNioClientDemo() {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(...);
                        }
                    })
                    .connect(host, port);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            group.shutdownGracefully();
        }
    }

重点是整条路线都是 Nio,这注定无法和 Java 原生 Socket 两个结合。

经过一番研究,发现可以这样用:

    private void nettyCall() {
        ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
        connectivityManager.requestNetwork(
                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR).build(),
                new ConnectivityManager.NetworkCallback() {
                    @Override
                    public void onAvailable(@NonNull Network network) {
                        nettyCallWebServer(network);
                    }
                }
        );
        connectivityManager.requestNetwork(
                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(),
                new ConnectivityManager.NetworkCallback() {
                    @Override
                    public void onAvailable(@NonNull Network network) {
                        nettyCallWebServer(network);
                    }
                }
        );
    }

    private void nettyCallWebServer(Network network) {
        EventLoopGroup group = new OioEventLoopGroup();
        try {
            String host = "2024.ip138.com";
            new Bootstrap()
                    .group(group)
                    .channelFactory(new SocketFactoryChannelFactory(network.getSocketFactory()))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new HttpClientCodec());
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            ch.pipeline().addLast(new HttpContentDecompressor());
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    URI uri = new URI("/");
                                    FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toASCIIString());
                                    request.headers().add(HttpHeaderNames.HOST, "2024.ip138.com");
                                    request.headers().add(HttpHeaderNames.ACCEPT, "text/html");
                                    request.headers().add(HttpHeaderNames.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0");
                                    ctx.writeAndFlush(request);
                                }

                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                    if (msg instanceof FullHttpResponse) {
                                        FullHttpResponse response = (FullHttpResponse) msg;
                                        String result = response.content().toString(CharsetUtil.UTF_8);
                                        String[] lines = result.split("\\r?\\n");
                                        for (String line : lines) {
                                            if (line.contains("IP")) {
                                                Log.w("RESULT", "nettyCallWebServer:" + line);
                                                break;
                                            }
                                        }
                                        ctx.close();
                                    }
                                }
                            });
                        }
                    })
                    .connect(host, 80)
                    .sync()
                    .channel()
                    .closeFuture()
                    .sync();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            group.shutdownGracefully();
        }
    }

这里是使用 Netty 作为一个 Http 客户端来使用(为了请求 IP 回显的 WEB 服务),其他场景原理是一样。

其中 SocketFactoryChannelFactory 是自定义的,源码也很简单:

public class SocketFactoryChannelFactory implements ChannelFactory<SocketChannel> {

    private final SocketFactory mSocketFactory;

    public SocketFactoryChannelFactory(SocketFactory socketFactory) {
        mSocketFactory = socketFactory;
    }

    @Override
    public SocketChannel newChannel() {
        try {
            return new OioSocketChannel(null, mSocketFactory.createSocket());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

原理是通过自定义 channelFactory 来代替 channel 函数的功能。目的是能够向 OioSocketChannel 传入 SocketFactory 的生产出来的 Socket 对象。实现从指定网卡收发数据的效果。

调用 nettyCall 的打印如下:

2024-03-26 23:15:22.202 26243-27198 RESULT com.liux.android.myapplication W  nettyCallWebServer:	<span class="F">IP: 116.169.10.10</span>Node information: wangtong144
2024-03-26 23:15:22.242 26243-27206 RESULT com.liux.android.myapplication W  nettyCallWebServer:	<span class="F">IP: 119.4.66.194</span>Node information: PS-000-01UmF141

可以看到,已经实现了从不同网卡发出请求。这里打印和 OkHttp 不一样是因为这个IP回显服务做了限流,测试次数过于频繁已经被拉黑了。。。

打完收工

总结,NettyAndroid 系统应用指定网卡的要点就俩:

  1. 通过 ConnectivityManager 获取指定网络的 Network 对象,进而获得对应的 SocketFactory 实例。
  2. 通过自定义 NettyChannelFactory 来构造使用自定义 Socket 实例的 OioSocketChannel 实例,整个过程会让 Netty 使用 Oio 而非 Nio

另外,上面 DEMONetworkCallback 只考虑了连接可用的场景,实际使用的时候也要考虑网络连接状态变化等情况。

  • 本文作者: 6x
  • 本文链接: https://6xyun.cn/article/202
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-ND 许可协议。转载请注明出处!