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
能够和具体类型的网络关联。
换句话说,这个 Network
中 SocketFactory
创建的 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 的目的有两个:
- 借助 Netty 封装的 NIO,实现高性能的服务器。
- 借助 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回显服务做了限流,测试次数过于频繁已经被拉黑了。。。
打完收工
总结,Netty
在 Android
系统应用指定网卡的要点就俩:
- 通过
ConnectivityManager
获取指定网络的Network
对象,进而获得对应的SocketFactory
实例。 - 通过自定义
Netty
的ChannelFactory
来构造使用自定义Socket
实例的OioSocketChannel
实例,整个过程会让Netty
使用Oio
而非Nio
。
另外,上面 DEMO
的 NetworkCallback
只考虑了连接可用的场景,实际使用的时候也要考虑网络连接状态变化等情况。