Android KK(4.4) 以太网和DHCP启动过程介绍
常做Android的大佬们可能知道,Android 5.0是一个历史分水岭一样的版本,其前后改动应该是所有版本中最大的了,而目前我的工作主要就围绕着Android P(9.0)和Android KK(4.4)两个版本进行支持,再了解熟悉了9.0的网络流程后,乘胜追击熟悉下Android 4.4 的网络模块,在源码基础上对以太网的初始化流程、网络策略配置、dhcp交互过程等做一些简单的介绍,老规矩先上图。
1. ethernet启动流程
这次文章写的稍微简单了些,只是简单介绍下流程,没有具体分析代码,如果对具体代码感兴趣可以看下Android network框架分析,本篇更多在介绍流程上的概述,话不多说开搞!
-
创建ConnectivityService
frameworks\base\services\java\com\android\server\SystemServer.java
networkmanagement、networkStats、networkPolicy已经提前创建好,并作为参数传入connectivity = new ConnectivityService(context,networkManagement,networkStats,networkPolicy);
-
创建NetworkStateTracker和RadioAttributes
frameworks\base\services\java\com\android\server\ConnectivityService.java
会创建多个Tracker,包括wifi、bluetooth、mobile、ethernet等mNetTrackers = new NetworkStateTracker[ConnectivityManager.MAX_NETWORK_TYPE+1];
mRadioAttributes = new RadioAttributes[ConnectivityManager.MAX_RADIO_TYPE+1]; -
加载网络策略配置属性
frameworks\base\services\java\com\android\server\ConnectivityService.java
// Load device network attributes from resources String[] raStrings = context.getResources().getStringArray( com.android.internal.R.array.radioAttributes); for (String raString : raStrings) { RadioAttributes r = new RadioAttributes(raString); if (VDBG) log("raString=" + raString + " r=" + r); if (r.mType > ConnectivityManager.MAX_RADIO_TYPE) { loge("Error in radioAttributes - ignoring attempt to define type " + r.mType); continue; } if (mRadioAttributes[r.mType] != null) { loge("Error in radioAttributes - ignoring attempt to redefine type " + r.mType); continue; } mRadioAttributes[r.mType] = r; }
-
对已支持的网络接口进行优先级排序
frameworks\base\services\java\com\android\server\ConnectivityService.java
// high priority first mPriorityList = new int[mNetworksDefined]; { int insertionPoint = mNetworksDefined-1; int currentLowest = 0; int nextLowest = 0; while (insertionPoint > -1) { for (NetworkConfig na : mNetConfigs) { if (na == null) continue; if (na.priority < currentLowest) continue; if (na.priority > currentLowest) { if (na.priority < nextLowest || nextLowest == 0) { nextLowest = na.priority; } continue; } mPriorityList[insertionPoint--] = na.type; } currentLowest = nextLowest; nextLowest = 0; } }
-
启动已支持网络接口的各个tracker
frameworks\base\services\java\com\android\server\ConnectivityService.java
// Create and start trackers for hard-coded networks for (int targetNetworkType : mPriorityList) { final NetworkConfig config = mNetConfigs[targetNetworkType]; final NetworkStateTracker tracker; try { tracker = netFactory.createTracker(targetNetworkType, config); mNetTrackers[targetNetworkType] = tracker; } catch (IllegalArgumentException e) { Slog.e(TAG, "Problem creating " + getNetworkTypeName(targetNetworkType) + " tracker: " + e); continue; } // 启动该网络接口的监测 tracker.startMonitoring(context, mTrackerHandler); if (config.isDefault()) { tracker.reconnect(); } }
-
启动配置文件指定的以太网接口
frameworks\base\core\java\android\net\EthernetDataTracker.java
EthernetDataTracker 将会寻找第一个以 eth 开头的有线网络设备,打开并开始做 dhcp<!-- Regex of wired ethernet ifaces --> <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
sIfaceMatch = context.getResources().getString( com.android.internal.R.string.config_ethernet_iface_regex); try { final String[] ifaces = mNMService.listInterfaces(); for (String iface : ifaces) { // 如果指定的接口存在于网络服务接口列表中,则通过该接口连接网络 if (iface.matches(sIfaceMatch)) { mNMService.setInterfaceUp(iface); InterfaceConfiguration config = mNMService.getInterfaceConfig(iface); if (getEthernetCarrierState(iface) == 1) { mIface = iface; mLinkUp = true; mNetworkInfo.setIsAvailable(true); if (config != null && mHwAddr == null) { mHwAddr = config.getHardwareAddress(); if (mHwAddr != null) { mNetworkInfo.setExtraInfo(mHwAddr); } } } // if a DHCP client had previously been started for this interface, then stop it NetworkUtils.stopDhcp(iface); } } reconnect(); } catch (RemoteException e) { Log.e(TAG, "Could not get list of interfaces " + e); }
-
启动DHCP服务
frameworks\base\core\java\android\net\EthernetDataTracker.java
public boolean reconnect() { if (mLinkUp) { mTeardownRequested.set(false); runDhcp(); } return mLinkUp; }
private void runDhcp() { Thread dhcpThread = new Thread(new Runnable() { public void run() { DhcpResults dhcpResults = new DhcpResults(); if (!NetworkUtils.runDhcp(mIface, dhcpResults)) { // 如果启动DHCP服务失败,则什么都不做退出 Log.e(TAG, "DHCP request error:" + NetworkUtils.getDhcpError()); return; } // 如果启动DHCP服务成功,则设置对应的连接状态,并通知其他网络服务模块更新状态 mLinkProperties = dhcpResults.linkProperties; mNetworkInfo.setIsAvailable(true); mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddr); Message msg = mCsHandler.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); msg.sendToTarget(); } }); dhcpThread.start(); }
-
以太网的网络策略配置
frameworks\base\core\res\res\values-large\config.xml
设置以太网的优先级,第四列数值越大、优先级越高<string-array translatable="false" name="networkAttributes"> <item>"wifi,1,1,2,-1,true"</item> <item>"bluetooth,7,7,0,-1,true"</item> <item>"ethernet,9,9,9,-1,true"</item> </string-array>
增加允许连接的网络类型,9表示以太网接口
<string-array translatable="false" name="radioAttributes"> <item>"1,1"</item> <item>"7,1"</item> <item>"9,1"</item> </string-array>
修改init.rc,增加以太网的service
service dhcpcd_eth0 /system/bin/dhcpcd -ABDHKLd class main disabled oneshot
2. DHCP启动过程
2.1 JNI层接口
Java层调用的NetworkUtils.runDhcp(mIface, dhcpResults)接口,在frameworks\base\core\jni\android_net_NetUtils.cpp
中定义
static jboolean android_net_utils_runDhcp(JNIEnv* env, jobject clazz, jstring ifname, jobject info)
{
return android_net_utils_runDhcpCommon(env, clazz, ifname, info, false);
}
static jboolean android_net_utils_runDhcpCommon(JNIEnv* env, jobject clazz, jstring ifname,
jobject dhcpResults, bool renew)
{
......
const char *nameStr = env->GetStringUTFChars(ifname, NULL);
if (nameStr == NULL) return (jboolean)false;
if (renew) {
result = ::dhcp_do_request_renew(nameStr, ipaddr, gateway, &prefixLength,
dns, server, &lease, vendorInfo, domains, mtu);
} else {
result = ::dhcp_do_request(nameStr, ipaddr, gateway, &prefixLength,
dns, server, &lease, vendorInfo, domains, mtu);
}
// 下面一段是将获取到的ip地址字段存储到JNI变量中供java层获取,此处省略
....
}
2.2 DHCP客户端(libnetutils)
dhcp_do_request接口定义在system\core\libnetutils\dhcp_utils.c
中,该接口主要做了以下几个工作:
-
启动DHCP服务,并等待直至DHCP准备OK
property_set(ctrl_prop, daemon_cmd);
wait_for_property(daemon_prop_name, desired_status, 10) -
等待DHCP服务端返回一个结果
wait_for_property(result_prop_name, NULL, 30)
-
如果DHCP返回的结果状态值为OK,则从DHCP服务端设置的全局属性中读取ip地址、网关等信息
fill_ip_info(interface, ipaddr, gateway, prefixLength, dns, server, lease, vendorInfo, domain, mtu)
2.3 DHCP服务端(dhcpcd)
-
DHCP消息类型
DHCPDISCOVER
DHCPOFFER
DHCPREQUEST
DHCPDECLINE
DHCPACK
DHCPNACK
DHCPRELEASE
DHCPINFORM
-
DHCP的四步租约过程
DHCP租约过程就是DHCP客户机动态获取IP地址的过程。
①客户机请求IP(客户机发DHCPDISCOVER广播包)
当一个DHCP客户机启动时,会自动将自己的IP地址配置成0.0.0.0,由于使用0.0.0.0不能进行正常通信,所以客户机就必须通过DHCP服务器来获取一个合法的地址。
由于客户机不知道DHCP服务器的IP地址,所以它使用0.0.0.0的地址作为源地址,使用UDP68端口作为源端口,使用255.255.255.255作为目标地址,使用UDP67端口作为目的端口来广播请求IP地址信息。
广播信息中包含了DHCP客户机的MAC地址和计算机名,以便使DHCP服务器能确定是哪个客户机发送的请求。
②服务器响应(服务器发DHCPOFFER广播包)
当DHCP服务器接收到客户机请求IP地址的信息时,它就在自己的IP地址池中查找是否有合法的IP地址提供给客户机。如果有,DHCP服务器就将此IP地址做上标记,加入到DHCPOFFER的消息中,然后DHCP服务器就广播一则包括下列信息的DHCPOFFER消息:
DHCP客户机的MAC地址
DHCP服务器提供的合法IP地址
子网掩码
默认网关(路由)
租约的期限
DHCP服务器的IP地址
因为DHCP客户机还没有IP地址,所以DHCP服务器使用自己的IP地址作为源地址,使用UDP67端口作为源端口,使用255.255.255.255作为目标地址,使用UDP68端口作为目的端口来广播DHCPOFFER信息。
③客户机选择IP(客户机发DHCPREQUEST广播包)
DHCP客户机从接收到的第一个DHCPOFFER消息中选择IP地址,发出IP地址的DHCP服务器将该地址保留,这样该地址就不能提供给另一个DHCP客户机。当客户机从第一个DHCP服务器接收DHCPOFFER并选择IP地址后,DHCP租约的第三过程发生。
客户机将DHCPREQUEST消息广播到所有的DHCP服务器,表明它接受提供的内容。DHCPREQUEST消息包括为该客户机提供IP配置的服务器的服务标识符(IP地址)。DHCP服务器查看服务器标识符字段,以确定它自己是否被选择为指定的客户机提供IP地址,如果那些DHCPOFFER被拒绝,则DHCP服务器会取消提供并保留其IP地址以用于下一个IP租约请求。
在客户机选择IP的过程中,虽然客户机选择了IP地址,但是还没有配置IP地址,而在一个网络中可能有几个DHCP服务器,所以客户机仍然使用0.0.0.0的地址作为源地址,使用UDP68端口作为源端口,使用255.255.255.255作为目标地址,使用UDP67端口作为目的端口来广播DHCPREQUEST信息。
④服务器确定租约(服务器发DHCPACK/DHCPNAK广播包)
DHCP服务器接收到DHCPREQUEST消息后,以DHCPACK消息的形式向客户机广播成功的确认,该消息包含有IP地址的有效租约和其他可能配置的信息。
虽然服务器确认了客户机的租约请求,但是客户机还没有收到服务器的DHCPACK消息,所以服务器仍然使用自己的IP地址作为源地址,使用UDP67端口作为源端口,使用255.255.255.255作为目标地址,使用UDP68端口作为目的端口来广播DHCPACK信息。
当客户机收到DHCPACK消息时,它就配置了IP地址,完成了TCP/IP的初始化。如果DHCPREQUEST不成功,例如客户机试图租约先前的IP地址,但该IP地址不再可用,或者因为客户机移到其他子网,该IP无效时,DHCP服务器将广播否定确认消息DHCPNAK。
当客户机接收到不成功的确认时,它将重新开始DHCP租约过程。如果DHCP客户机无法找到DHCP服务器,它将从TCP/IP的B类网段169.254.0.0中挑选一个IP地址作为自己的IP地址,继续每隔5分钟尝试与DHCP服务器进行通讯,一旦与DHCP服务器取得联系,则客户机放弃自动配置的IP地址,而使用DHCP服务器分配的IP地址。
-
DHCP客户机向网络DHCP服务器租约IP地址的流程及实际log
dhcpcd[1544]: version 5.5.6 starting dhcpcd[1544]: get_duid: Permission denied dhcpcd[1544]: eth0: using ClientID 01:12:78:e4:0b:44:00 dhcpcd[1544]: eth0: executing `/system/etc/dhcpcd/dhcpcd-run-hooks', reason PREINIT dhcpcd[1544]: eth0: reading lease `/data/misc/dhcp/dhcpcd-eth0.lease' dhcpcd[1544]: eth0: rebinding lease of 192.168.4.164 dhcpcd[1544]: eth0: sending REQUEST (xid 0x440be4), next in 3.54 seconds dhcpcd[1544]: eth0: sending REQUEST (xid 0x440be4), next in 7.63 seconds dhcpcd[1544]: eth0: NAK: from 192.168.4.1 dhcpcd[1544]: eth0: executing `/system/etc/dhcpcd/dhcpcd-run-hooks', reason NAK dhcpcd[1544]: eth0: broadcasting for a lease dhcpcd[1544]: eth0: sending DISCOVER (xid 0x440be4), next in 4.97 seconds dhcpcd[1544]: eth0: offered 192.168.4.165 from 192.168.4.1 dhcpcd[1544]: eth0: sending REQUEST (xid 0x440be4), next in 3.02 seconds dhcpcd[1544]: eth0: acknowledged 192.168.4.165 from 192.168.4.1 dhcpcd[1544]: eth0: leased 192.168.4.165 for 7200 seconds dhcpcd[1544]: eth0: adding IP address 192.168.4.165/24 dhcpcd[1544]: eth0: adding route to 192.168.4.0/24 dhcpcd[1544]: eth0: adding default route via 192.168.4.1 dhcpcd[1544]: eth0: writing lease `/data/misc/dhcp/dhcpcd-eth0.lease' dhcpcd[1544]: eth0: executing `/system/etc/dhcpcd/dhcpcd-run-hooks', reason BOUND
-
调试网络的常用命令
-
netcfg
netcfg //查看ip情况
netcfg eth0 up dhcp //通过dhcp 自动获取ip和网关 -
ifconfig
ifconfig eth0 192.168.8.81 up
ifconfig eth0 192.168.8.81 netmask 255.255.255.0 up -
gateway 配置
route add default gw 192.168.0.1 dev eth0 -
dns 配置
setprop net.dns1 192.168.8.11
setprop net.dns2 147.11.100.30 -
mac adddr
ifconfig eth0 hw ether 00:AA:BB:33:44:55
-