网络
OKHttp中body().string()
其中string()
只能被使用一次,使用过后就会被清空掉。即代码中调用两次body().string()
的话第二次是没有值的。如果想要则需要用个临时变量保存起来。
Regrofit
OkHttpClickent
配置超时时间
默认情况下,Retrofit
的默认超时时间如下:
Connection timeout
:10秒Read timeout
:10秒Write timeout
:10秒Call timeout
:0秒(代表没有超时)
如何设置超时时间
1 | OkHttpClient okHttpClient = new OkHttpClient.Builder() |
Connection Timeout-连接超时
connectionTimeout
:从客户端发出一个请求开始到客户端与服务器端完成TCP
的3次握手建立连接的这段时间。即,如果Retrofit
在指定的时间无法与服务器端建立连接,那么Retrofit
就认为此次请求失败。
比如,当你的用户可能会在网络状态不佳的情况下与你的服务器进行通信,那么你需要将增大这个数字。
Read Timeout-读取超时
readTimeout
:从连接建立成功开始,Retrofit
就会监测每个字节的数据传输的速率。如果其中某字节距离上一个字节传输成功的时间大于指定的readTimeout
了,Retrofit
就会认为这个请求是失败的。这个时间计数器会在读取到每个byte
之后归零重新开始计时。所以如果你的响应当中有120个bytes
需要传输到客户端,而每个byte
的传输都需要5秒,这种情况下尽管完全传输需要600秒,但不会触发readTimeout(30秒)error
。
另外,readTimeout
的触发不仅限于服务器端的处理能力,也有可能是糟糕的网络状态引起。
注意这个并不是说在指定的时间(比如30秒)内需要把响应内容完全接收,而是指相邻的两个字节之间的接收时间不能超过指定的时间(30秒)。
Write Timeout-写入超时
writeTimeout
:跟readTimeout
相对应的反方向的数据传输。它检查的是客户端向服务器端发送数据的速率。当然,跟readTimeout
的计时器类似,每个byte
发送成功之后这个计时器都会被重置。如果某个byte
的数据传输时间 超过了配置的写入超时时间,Retrofit
就会认为这个请求是失败的。
注意这个并不是说在指定时间(比如15秒)内需要把所有的数据都发送到服务器端,而是指相邻的两个字节之间的发送时间不能超过指定的时间(15秒)。
Call Timeout-请求超时
可从这看 https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout/
CallTimeout
的计时器横跨整个请求,从DNS
解析,连接建立,发送数据到服务器,服务器端处理,然后发送响应到客户端,直到客户端完全读取响应内容。如果这个请求需要重定向或重试,这些过程都必须在指定的callTimeout
时间区间内完成。如果不能完成Retrofit
就会认为请求失败。
CallTimeout=0
:代表不考虑请求的超时
当你的应用需要限定
App
在某个指定的时间内得到响应结果,如果在指定时间内未能得到就认为是超时的话,那么你应该用callTimeout
利用Android的以太网共享功能模拟LAN网口
本文的目标是在一个安装了 Android 系统的设备上模拟软路由。这个设备拥有三个网口,最终的目标是希望模拟出一个 WAN网口和两个 LAN网口。目标系统为 Android 14。
Android14提供了以太网共享网络的功能,借助这个功能,我们可以将一个网口模拟成LAN口,但可惜的是,这个功能只对一个网口生效。我们先简要分析一下其实现原理,然后再给出在有三个网口的机器上模拟出两个LAN接口的技术解决方案。
代码分析
Settings 设置
我们从系统已有的功能出发。网络共享的功能可以在原生 Settings 中找到相应开关,因此也可以找到相应的开启以太网共享的 SDK 接口:
1 | // packages/apps/Settings/src/com/android/settings/network/tether/TetherSettings.java |
当我们点击界面上的 Switch 时,onPreferenceTreeClick()
方法被回调,Settings 根据点击的按钮不同,将会使用不同的 Choice 来调用startTethering
方法。
TetherSettings#startTethering()
方法调用了ConnectivityManager#startTethering()
来启用某一种网络共享,网络共享的具体通道(蓝牙、USB 、以太网)则由参数 choice 来决定。
ConnectivityManager 连接管理器
1 | // packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java |
ConnectivityManager#startTethering()
这个 API 已经被标记为废弃了。它的实现的大意为,将回调和网络共享type
的信息进一步封装起来,并传递给TetheringManager
进行处理。getTetheringManager()
的实现告诉我们 TetheringManager 实际上也对应一个 System Server,为了节约篇幅,我们跳过 Manager 直接关注其背后实现 Server 的代码:
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/TetheringService.java |
TetheringService
在完成参数校验后,委托给Tethering
类来处理共享的具体逻辑。
Tethering 网络共享
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/Tethering.java |
Tethering
类可以处理多种不同通道的网络共享,我们重点关注的以太网共享功能,其实现关键位于setEthernetTethering()
这个方法。
这个方法大意为,首先获取EthernetManager
,然后调用requestTetheredInterface
来尝试获取一个可用的以太网接口,Tethering
将会在这个接口上开启一系列关键的服务,如 DHCP 等。requestTetheredInterface
的结果将通过EthernetCallback
这个回调接口来回传。我们先观察一下EthernetCallback
的实现:
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/Tethering.java |
Tethering
获得这个接口的信息后,用这个接口的名称来调用enableIpServing
。enableIpServing
将会开启绑定在这个接口上的IpServer
,这个类提供了 DHCP 等关键服务,对实现网络共享至关重要。
EthernetManager 以太网管理器
我们跳过EthernetManager#requestTetheredInterface()
,直接找到真正的实现EthernetServiceImpl#requestTetheredInterface()
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java |
EthernetServiceImpl
又将具体的实现委托给了EthernetTracker
来执行。而我们会注意到,EthernetTracker
将直接尝试返回类变量mTetheringInterface
,这个变量将作为以太网的共享接口。
EthernetTracker
对 mTetheringInterface 这个特殊的共享接口进行了许多特殊操作,另外两个与之相关的类变量是mTetheredInterfaceWasAvailable
和mTetheringInterfaceMode
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
mTetheringInterface 将在maybeTrackInterface
这个方法内被选举出来:
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
简单来说,第一个没有配置NetworkCapabilities
的以太网接口将被作为共享接口。
最后,我们关注一下isValidEthernetInterface
的实现,这个方法是EthernetTracker
判断一个接口是不是以太网接口的通用方式。
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
据此我们可以得出结论,确认一个接口是否是以太网接口的方式是用正则表达式对接口的名字做匹配。相关的字符串资源文件位于下方:
1 | // packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config.xml |
显然,默认情况下,只有以 eth 开头,并且后面接一位数字的接口才会被认为是以太网接口。
如何获得EthernetTracker
的信息?
通过 dumpsys ethernet
。
信息的含义请参考EthernetTracker
的dump
方法。
随机子网
无论我们是使用 Wifi 热点还是以太网网络共享,设备虚拟出来的子网是随机的,生成这些随机子网的逻辑藏在IpServer
内:
1 | // packages/modules/Connectivity/Tethering/src/android/net/ip/IpServer.java |
IpServer
调用了PrivateAddressCoordinator
的相关方法来生成随机的地址。
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java |
mCachedAddresses
是AddressKey
到LinkAddress
之间的映射,从第44行AddressKey
的equals
方法实现来看,同一个类型的网络共享一般会命中同一个 cache,因此,一般来说同一个类型的网络分享在关闭又重新开启后,使用的依然是之前计算好的随机子网。
网络分享内的设备为什么无法互通
阻碍来自 iptables 的转发规则,网络共享的转发规则会专门存放在 tetherctrl_FORWARD 表中(这个表被FORWARD表进一步引用),它的默认规则直接 DROP 掉了局域网内的流量,这直接导致通过网络共享功能连接到 Android 系统的各个设备,相互之间无法成功通信。
我们可以通过以下命令来查看 iptables 规则:
1 | iptables -nvL |
如果拥有 su 权限,我们可以使用下述命令来删除 tetherctrl_FORWARD 表中的规则:
1 | iptables -D tetherctrl_FORWARD -j DROP |
但是这是一种治标不治本的方式,每次网络变动(比如插拔网线), tetherctrl_FORWARD 表会被重置。因此我们必须找到相关的代码来修改其行为。
system/netd/server/TetherController.cpp 中包含了与以太网网络共享相关的 iptables 修改逻辑,我们这里指出其中两个关键的方法:
1 | // system/netd/server/TetherController.cpp |
根据实际效果而言,setDefaults()
方法中填写的规则将会在 WAN 口尚未接入网线但开启了网络共享功能时生效。
1 | int TetherController::setForwardRules(bool add, const char *intIface, const char *extIface) { |
setForwardRules()
方法将会在开启了网络共享功能后, WAN 口接入网线时生效。我们可以看到实现重新添加了DROP
规则。
修改 iptables 规则需要小心,一旦上述规则有任何的执行错误,网络共享功能就会被停止。比如说,我们强行在命令行中影响了 iptables 规则,就有可能导致网络共享功能在网线接入或移除时被停止。
我们举一个例子,比如我们手动删除了 DROP 规则,根据上述代码,我们知道,网线接入时TetherController
也会尝试删除这个规则,由于我们提前删除了,所以上述代码的第 4 行规则在执行时会出错,这个错误就会导致网络共享功能被停止。总之,网线接入/移除时网络共享功能莫名停止,多半是 iptables 规则不对。
解决方案
根据上面的代码调研,我们发现, SDK 在多处实现中都假设以太网网络共享只会分享一个接口,原生系统无法达到获得两个类似于路由器上的 LAN 口的效果,我们必须加以改造。
改造的思路大致有两种:
- 创建一个以太网网桥,并将 eth1 和 eth2 连接到网桥上,网络共享则统一使用这个网桥。
- 修改 SDK 实现,让 eth1 和 eth2 同时开启
IpServer
,每个接口将分配到不同的子网上。
网桥
我们这里省略如何编译 Android 系统上可用的网桥控制命令 brctl ,重点关注如何利用它创建网桥。我们假设 brctl 将安装到 /system/bin 下。首先,我们在 /system/bin/ 下编写一个脚本 init.brctl.sh
1 | !/bin/sh |
然后,我们使用 initrc 机制让这个脚本在开机时得以执行:
1 | service brctl_init /system/bin/init.brctl.sh |
完成这一步后,我们就可以在开机后得到一个新的接口br0
,它是我们虚拟出来的以太网接口。接下来,我们需要稍微修改 framework 代码,使得这个接口可以被识别为以太网接口,并保证它一定被选择作为以太网共享接口。
首先,我们修改 xml 资源文件:
1 | // packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config.xml |
然后,我们修改EthernetTracker
,让它固定选中 br0 接口用来以太网共享。
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
为了让 Settings 也能够把网桥识别为以太网共享,从而可以正常通过开关开启和关闭共享,需要修改TetherSettings
内的正则表达式,如下:
1 | // packages/apps/Settings/src/com/android/settings/network/tether/TetherSettings.java |
同时开启两个网口的共享
与第一种思路相比,这个思路的实现相对来说复杂一些,但涉及到的文件很少,只需要修改Tethering
和EthernetTracker
即可。
对于Tethering
的修改集中在将变量mConfiguredEthernetIface
替换为集合mConfiguredEthernetIfaceSet
,并让Tethering
开启两个IpServer
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/Tethering.java |
对EthernetTracker
的修改会比较多,但思路仍然是用集合代替单一的接口变量。我们声明一个新的内部类用于同时保存接口名、状态和可用性,并打算用 HashMap 来平替之前的变量。
1 | // packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
接下来就是非常琐碎的变量平替修改,我们这里仅记录 diff
1 | diff --git a/packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java b/packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java |
开机后自动开启网络共享
重启后,Settings 并不会保存网络共享的状态,因此我们需要保证下述代码在开机后会执行一遍。
1 | ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class); |
iptables
对于默认规则,我们强行让其变为ACCEPT
1 | // system/netd/server/TetherController.cpp |
接下来,我们不允许其重置 DROP 规则,包括移除和添加。(在没有 DROP 规则的情况下尝试移除也会报错)
1 | // system/netd/server/TetherController.cpp |
固定共享子网地址
修改PrivateAddressCoordinator
这个类即可。我们可以通过IpServer
获得当前共享的网络接口的名称,根据这个名称我们提前拦截随机分配的逻辑,下面展示双网口共享的修改方式:
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java |
单网桥的修改方式类似
1 | // packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java |
参考资料:
网络相关问题
java.lang.RuntimeException: Parcel: unable to marshal value
接口回调传递参数丢失问题
原因:
- 在继承Parcel类中,需要读或写其他自定义类数据,这些自定义类数据需要实现Serializable序列化接口
- 在继承Serializabel类中,需要读或写其他自定义类,这些自定义类数据含有的对象类没有继承Serializable接口