参考:
1)DHCP的一些解释
2)DHCP详细工作过程
WiFi的连接过程直到现在还没完全梳理清楚,大致知道其中几块,app到framework的流程大致梳理过,后续是到wpa_supplicant的四次握手,这后面不知道了,接着会到DhcpClient的ip地址获取。本文主要学习下WiFi连接过程中的dhcp。(有时间好好梳理下)
以下参考自DHCP的一些解释,谢谢分享,细节可以参考DHCP详细工作过程。
DHCP,动态主机配置协议,前身是BOOTP协议,是一个局域网的网络协议,使用UDP协议工作,常用的2个端口:67(DHCP server),68(DHCP client)。DHCP通常被用于局域网环境,主要作用是集中的管理、分配IP地址,使client动态的获得IP地址、Gateway地址、DNS服务器地址等信息,并能够提升地址的使用率。简单来说,DHCP就是一个不需要账号密码登录的、自动给内网机器分配IP地址等信息的协议。
DHCP工作流程:
原则上DHCP SERVER是一直处在被动接受请求的状态,当有客户端请求时,服务器会读取获得客户端当前所在的状态以及客户端的信息,并在静态租约表和动态租约表中进行检索找到相应的表项,再根据客户端的状态执行不同的回复。当收到客户端的首次请求时,DHCP服务器先查找静态租约表;若存在请求的表项,返回这个客户的静态IP地址;否则,从IP地址池中选择可用的IP分配给客户,并添加信息到动态数据库中。此外,服务器将会周期性的刷新租约表写入文件存档,在这个过程中会顺便对动态租约表进行租期检查。
执行回复动作:
用小米 mix2 8.0抓取了如下WiFi连接过程中Dhcp log
09-01 09:29:37.136 1561 2458 D DhcpClient: doQuit
09-01 09:29:37.150 1561 26809 D DhcpClient: Receive thread stopped
09-01 09:29:37.152 1561 26806 D DhcpClient: onQuitting
//断开WiFi
09-01 09:29:43.516 1561 27101 D DhcpClient: Receive thread started
09-01 09:29:43.519 1561 27099 D DhcpClient: Broadcasting DHCPDISCOVER
09-01 09:29:43.523 1561 27101 D DhcpClient: Received packet: e4:46:da:6b:f5:50 OFFER, ip /192.168.0.105, mask /255.255.255.0, DNS servers: /192.168.0.1 , gateways [/192.168.0.1] lease time 7200, domain null
09-01 09:29:43.524 1561 27099 D DhcpClient: Got pending lease: IP address 192.168.0.105/24 Gateway 192.168.0.1 DNS servers: [ 192.168.0.1 ] Domains DHCP server /192.168.0.1 Vendor info null lease 7200 seconds
09-01 09:29:43.524 1561 27099 D DhcpClient: Broadcasting DHCPREQUEST ciaddr=0.0.0.0 request=192.168.0.105 serverid=192.168.0.1
09-01 09:29:43.528 1561 27101 D DhcpClient: Received packet: e4:46:da:6b:f5:50 ACK: your new IP /192.168.0.105, netmask /255.255.255.0, gateways [/192.168.0.1] DNS servers: /192.168.0.1 , lease time 7200
09-01 09:29:43.528 1561 27099 D DhcpClient: Confirmed lease: IP address 192.168.0.105/24 Gateway 192.168.0.1 DNS servers: [ 192.168.0.1 ] Domains DHCP server /192.168.0.1 Vendor info null lease 7200 seconds
//连接WiFi
光从log来看dhcp分为四步
DhcpClient
首先看到有Receive thread started的打印,看下逻辑
class ReceiveThread extends Thread {
private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
private volatile boolean mStopped = false;
public void halt() {
mStopped = true;
closeSockets(); // Interrupts the read() call the thread is blocked in.
}
@Override
public void run() {
if (DBG) Log.d(TAG, "Receive thread started");
while (!mStopped) {
int length = 0; // Or compiler can't tell it's initialized if a parse error occurs.
try {
length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
DhcpPacket packet = null;
packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
if (DBG) Log.d(TAG, "Received packet: " + packet);
sendMessage(CMD_RECEIVED_PACKET, packet);
} catch (IOException|ErrnoException e) {
if (!mStopped) {
Log.e(TAG, "Read error", e);
logError(DhcpErrorEvent.RECEIVE_ERROR);
}
} catch (DhcpPacket.ParseException e) {
Log.e(TAG, "Can't parse packet: " + e.getMessage());
if (PACKET_DBG) {
Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
}
if (e.errorCode == DhcpErrorEvent.DHCP_NO_COOKIE) {
int snetTagId = 0x534e4554;
String bugId = "31850211";
int uid = -1;
String data = DhcpPacket.ParseException.class.getName();
EventLog.writeEvent(snetTagId, bugId, uid, data);
}
logError(e.errorCode);
}
}
if (DBG) Log.d(TAG, "Receive thread stopped");
}
}
class DhcpState extends State {
@Override
public void enter() {
clearDhcpState();
if (initInterface() && initSockets()) {
mReceiveThread = new ReceiveThread();
mReceiveThread.start();
} else {
notifyFailure();
transitionTo(mStoppedState);
}
}
@Override
public void exit() {
if (mReceiveThread != null) {
mReceiveThread.halt(); // Also closes sockets.
mReceiveThread = null;
}
clearDhcpState();
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_STOP_DHCP:
transitionTo(mStoppedState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
DhcpClient中也包含了一个状态机,当进入DhcpState中的时候会将ReceiveThread线程启动起来,这个线程包含了一个while循环调用Os.read并解析packet的逻辑,当退出时会将mStopped置为true,线程while循环停止。
顺便看下这个类包含的状态机
// TODO: Take an InterfaceParams instance instead of an interface name String.
private DhcpClient(Context context, StateMachine controller, String iface) {
super(TAG, controller.getHandler());
mContext = context;
mController = controller;
mIfaceName = iface;
addState(mStoppedState);
addState(mDhcpState);
addState(mDhcpInitState, mDhcpState);
addState(mWaitBeforeStartState, mDhcpState);
addState(mDhcpSelectingState, mDhcpState);
addState(mDhcpRequestingState, mDhcpState);
addState(mDhcpHaveLeaseState, mDhcpState);
addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
addState(mDhcpBoundState, mDhcpHaveLeaseState);
addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState);
addState(mDhcpRenewingState, mDhcpHaveLeaseState);
addState(mDhcpRebindingState, mDhcpHaveLeaseState);
addState(mDhcpInitRebootState, mDhcpState);
addState(mDhcpRebootingState, mDhcpState);
setInitialState(mStoppedState);
接着看下Broadcasting DHCPDISCOVER
private boolean sendDiscoverPacket() {
ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
DO_UNICAST, REQUESTED_PARAMS);
return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
}
// DHCP parameters that we request.
/* package */ static final byte[] REQUESTED_PARAMS = new byte[] {
DHCP_SUBNET_MASK,
DHCP_ROUTER,
DHCP_DNS_SERVER,
DHCP_DOMAIN_NAME,
DHCP_MTU,
DHCP_BROADCAST_ADDRESS, // TODO: currently ignored.
DHCP_LEASE_TIME,
DHCP_RENEWAL_TIME,
DHCP_REBINDING_TIME,
DHCP_VENDOR_INFO,
};
先看下这个状态类
/**
* Retransmits packets using jittered exponential backoff with an optional timeout. Packet
* transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. If a subclass
* sets mTimeout to a positive value, then timeout() is called by an AlarmManager alarm mTimeout
* milliseconds after entering the state. Kicks and timeouts are cancelled when leaving the
* state.
*
* Concrete subclasses must implement sendPacket, which is called when the alarm fires and a
* packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET
* sent by the receive thread. They may also set mTimeout and implement timeout.
*/
abstract class PacketRetransmittingState extends LoggingState {
private int mTimer;
protected int mTimeout = 0;
@Override
public void enter() {
super.enter();
initTimer();
maybeInitTimeout();
sendMessage(CMD_KICK);
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_KICK:
sendPacket();
scheduleKick();
return HANDLED;
case CMD_RECEIVED_PACKET:
receivePacket((DhcpPacket) message.obj);
return HANDLED;
case CMD_TIMEOUT:
timeout();
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exit() {
super.exit();
mKickAlarm.cancel();
mTimeoutAlarm.cancel();
}
abstract protected boolean sendPacket();
abstract protected void receivePacket(DhcpPacket packet);
protected void timeout() {}
protected void initTimer() {
mTimer = FIRST_TIMEOUT_MS;
}
protected int jitterTimer(int baseTimer) {
int maxJitter = baseTimer / 10;
int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter;
return baseTimer + jitter;
}
protected void scheduleKick() {
long now = SystemClock.elapsedRealtime();
long timeout = jitterTimer(mTimer);
long alarmTime = now + timeout;
mKickAlarm.schedule(alarmTime);
mTimer *= 2;
if (mTimer > MAX_TIMEOUT_MS) {
mTimer = MAX_TIMEOUT_MS;
}
}
protected void maybeInitTimeout() {
if (mTimeout > 0) {
long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
mTimeoutAlarm.schedule(alarmTime);
}
}
}
scheduleKick的算法好像有点意思,timer默认是2s,经过jitterTimer后是1.8-2.2s,即附加了10%的抖动,然后以这个随机值作为timeout。kick命令在timeout之后发出。接着timer*2的间隙进行重发,用注释的意思来说就是指数抖动退避,上限是128s,即2^7。
这个状态类一进入就开始触发CMD_KICK进行sendPacket和sheduleKick的操作。
而之前一直while循环的ReceiveThread线程会触发receivepacket
class ReceiveThread extends Thread {
private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
private volatile boolean mStopped = false;
public void halt() {
mStopped = true;
closeSockets(); // Interrupts the read() call the thread is blocked in.
}
@Override
public void run() {
if (DBG) Log.d(TAG, "Receive thread started");
while (!mStopped) {
int length = 0; // Or compiler can't tell it's initialized if a parse error occurs.
try {
length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
DhcpPacket packet = null;
packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
if (DBG) Log.d(TAG, "Received packet: " + packet);
sendMessage(CMD_RECEIVED_PACKET, packet);
从初始状态StoppedState切换到DhcpInitState就会触发之前分析的sendpacket,然后如果ReceiveThread发送CMD_RECEIVED_PACKET则会触发receivePacket.
class DhcpInitState extends PacketRetransmittingState {
public DhcpInitState() {
super();
}
@Override
public void enter() {
super.enter();
startNewTransaction();
mLastInitEnterTime = SystemClock.elapsedRealtime();
}
protected boolean sendPacket() {
return sendDiscoverPacket();
}
protected void receivePacket(DhcpPacket packet) {
if (!isValidPacket(packet)) return;
if (!(packet instanceof DhcpOfferPacket)) return;
mOffer = packet.toDhcpResults();
if (mOffer != null) {
Log.d(TAG, "Got pending lease: " + mOffer);
transitionTo(mDhcpRequestingState);
}
}
}
class StoppedState extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_START_DHCP:
if (mRegisteredForPreDhcpNotification) {
transitionTo(mWaitBeforeStartState);
} else {
transitionTo(mDhcpInitState);
}
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
对应log:
09-01 09:29:43.519 1561 27099 D DhcpClient: Broadcasting DHCPDISCOVER
09-01 09:29:43.523 1561 27101 D DhcpClient: Received packet: e4:46:da:6b:f5:50 OFFER, ip /192.168.0.105, mask /255.255.255.0, DNS servers: /192.168.0.1 , gateways [/192.168.0.1] lease time 7200, domain null
09-01 09:29:43.524 1561 27099 D DhcpClient: Got pending lease: IP address 192.168.0.105/24 Gateway 192.168.0.1 DNS servers: [ 192.168.0.1 ] Domains DHCP server /192.168.0.1 Vendor info null lease 7200 seconds
对应发出的packet:
private boolean sendDiscoverPacket() {
ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
DO_UNICAST, REQUESTED_PARAMS);
return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
}
// DHCP parameters that we request.
/* package */ static final byte[] REQUESTED_PARAMS = new byte[] {
DHCP_SUBNET_MASK,
DHCP_ROUTER,
DHCP_DNS_SERVER,
DHCP_DOMAIN_NAME,
DHCP_MTU,
DHCP_BROADCAST_ADDRESS, // TODO: currently ignored.
DHCP_LEASE_TIME,
DHCP_RENEWAL_TIME,
DHCP_REBINDING_TIME,
DHCP_VENDOR_INFO,
};
继而进入到DhcpRequestingState状态
class DhcpRequestingState extends PacketRetransmittingState {
public DhcpRequestingState() {
mTimeout = DHCP_TIMEOUT_MS / 2;
}
protected boolean sendPacket() {
return sendRequestPacket(
INADDR_ANY, // ciaddr
(Inet4Address) mOffer.ipAddress.getAddress(), // DHCP_REQUESTED_IP
(Inet4Address) mOffer.serverAddress, // DHCP_SERVER_IDENTIFIER
INADDR_BROADCAST); // packet destination address
}
protected void receivePacket(DhcpPacket packet) {
if (!isValidPacket(packet)) return;
if ((packet instanceof DhcpAckPacket)) {
DhcpResults results = packet.toDhcpResults();
if (results != null) {
setDhcpLeaseExpiry(packet);
acceptDhcpResults(results, "Confirmed");
transitionTo(mConfiguringInterfaceState);
}
} else if (packet instanceof DhcpNakPacket) {
// TODO: Wait a while before returning into INIT state.
Log.d(TAG, "Received NAK, returning to INIT");
mOffer = null;
transitionTo(mDhcpInitState);
}
}
@Override
protected void timeout() {
// After sending REQUESTs unsuccessfully for a while, go back to init.
transitionTo(mDhcpInitState);
}
}
观察到DhcpRequestingState状态有timeout限制
// This is not strictly needed, since the client is asynchronous and implements exponential
// backoff. It's maintained for backwards compatibility with the previous DHCP code, which was
// a blocking operation with a 30-second timeout. We pick 36 seconds so we can send packets at
// t=0, t=2, t=6, t=14, t=30, allowing for 10% jitter.
private static final int DHCP_TIMEOUT_MS = 36 * SECONDS;
即这个状态18s会触发timeout,2^4+2,即timeout时限内允许发送5次packet。在时限内收到包,如果是DhcpAckPacket,则切换到ConfiguringInterfaceState,否则如果是DhcpNakPacket切换至DhcpInitState。
class ConfiguringInterfaceState extends LoggingState {
@Override
public void enter() {
super.enter();
mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case EVENT_LINKADDRESS_CONFIGURED:
transitionTo(mDhcpBoundState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
继续梳理把,mController是IpClient的startIPv4创建并开始流程的。
private boolean startIPv4() {
// If we have a StaticIpConfiguration attempt to apply it and
// handle the result accordingly.
if (mConfiguration.mStaticIpConfig != null) {
if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
} else {
return false;
}
} else {
// Start DHCPv4.
mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
mDhcpClient.registerForPreDhcpNotification();
mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
}
return true;
}
DhcpClient也处理CMD_START_DHCP开始状态变更,稍微等一下状态切换会回到mDhcpInitState开始。
class StoppedState extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_START_DHCP:
if (mRegisteredForPreDhcpNotification) {
transitionTo(mWaitBeforeStartState);
} else {
transitionTo(mDhcpInitState);
}
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
// Sends CMD_PRE_DHCP_ACTION to the controller, waits for the controller to respond with
// CMD_PRE_DHCP_ACTION_COMPLETE, and then transitions to mOtherState.
abstract class WaitBeforeOtherState extends LoggingState {
protected State mOtherState;
@Override
public void enter() {
super.enter();
mController.sendMessage(CMD_PRE_DHCP_ACTION);
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_PRE_DHCP_ACTION_COMPLETE:
transitionTo(mOtherState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
IPClient处理DhcpClient发来的CMD_CONFIGURE_LINKADDRESS
case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
final LinkAddress ipAddress = (LinkAddress) msg.obj;
if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
} else {
logError("Failed to set IPv4 address.");
dispatchCallback(ProvisioningChange.LOST_PROVISIONING,
new LinkProperties(mLinkProperties));
transitionTo(mStoppingState);
}
break;
}
设置玩ipv4 地址后DhcpClient再发送EVENT_LINKADDRESS_CONFIGURED切换到DhcpBoundState状态
class ConfiguringInterfaceState extends LoggingState {
@Override
public void enter() {
super.enter();
mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case EVENT_LINKADDRESS_CONFIGURED:
transitionTo(mDhcpBoundState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
class DhcpBoundState extends LoggingState {
@Override
public void enter() {
super.enter();
if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
// There's likely no point in going into DhcpInitState here, we'll probably
// just repeat the transaction, get the same IP address as before, and fail.
//
// NOTE: It is observed that connectUdpSock() basically never fails, due to
// SO_BINDTODEVICE. Examining the local socket address shows it will happily
// return an IPv4 address from another interface, or even return "0.0.0.0".
//
// TODO: Consider deleting this check, following testing on several kernels.
notifyFailure();
transitionTo(mStoppedState);
}
scheduleLeaseTimers();
logTimeToBoundState();
}
通过获取的服务器地址进行udp sock的连接。
private boolean connectUdpSock(Inet4Address to) {
try {
Os.connect(mUdpSock, to, DhcpPacket.DHCP_SERVER);
return true;
} catch (SocketException|ErrnoException e) {
Log.e(TAG, "Error connecting UDP socket", e);
return false;
}
}
先到这里吧。。。