Android熱點連線管理(一)
手機上的wifi功能,多半都被當做客戶端在使用。當做熱點共享網路時的場景比較少。 最近做一個嘗試,將所有試圖連線到Android便攜熱點的客戶端的資訊,通過底層一直上報上來,最終增加API供上層應用呼叫。 在原生的Android程式碼中,其實已經有一個WifiDevice類來表示當前連線至wifi熱點的客戶端資訊,我們先來看一下這個類是怎樣定義的。 /** * Describes information about a detected Wi-Fi STA. * {@hide} */ public class WifiDevice implements Parcelable { /** * The device MAC address is the unique id of a Wi-Fi STA */ public String deviceAddress = ""; /** * The device name is a readable string of a Wi-Fi STA */ public String deviceName = ""; /** * The device state is the state of a Wi-Fi STA */ public int deviceState = 0; /** * These definitions are for deviceState */ public static final int DISCONNECTED = 0; public static final int CONNECTED = 1; public static final int BLACKLISTED = 2; private static final String AP_STA_CONNECTED_STR = "AP-STA-CONNECTED"; private static final String AP_STA_DISCONNECTED_STR = "AP-STA-DISCONNECTED"; private static final String AP_STA_REPORT_STR = "AP-STA-REPORT"; /** {@hide} */ public WifiDevice() {} /** * @param string formats supported include * * AP-STA-CONNECTED 42:fc:89:a8:96:09 * AP-STA-DISCONNECTED 42:fc:89:a8:96:09 * * Note: The events formats can be looked up in the hostapd code * @hide */ public WifiDevice(String dataString) throws IllegalArgumentException { String[] tokens = dataString.split(" "); if (tokens.length < 2) { throw new IllegalArgumentException(); } if (tokens[0].indexOf(AP_STA_CONNECTED_STR) != -1) { deviceState = CONNECTED; } else if (tokens[0].indexOf(AP_STA_DISCONNECTED_STR) != -1) { deviceState = DISCONNECTED; }else if (tokens[0].indexOf(AP_STA_REPORT_STR) != -1) { deviceState = BLACKLISTED; } else { throw new IllegalArgumentException(); } deviceAddress = tokens[1]; } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof WifiDevice)) { return false; } WifiDevice other = (WifiDevice) obj; if (deviceAddress == null) { return (other.deviceAddress == null); } else { return deviceAddress.equals(other.deviceAddress); } } /** Implement the Parcelable interface {@hide} */ public int describeContents() { return 0; } /** Implement the Parcelable interface {@hide} */ public void writeToParcel(Parcel dest, int flags) { dest.writeString(deviceAddress); dest.writeString(deviceName); dest.writeInt(deviceState); } /** Implement the Parcelable interface {@hide} */ public static final Creator<WifiDevice> CREATOR = new Creator<WifiDevice>() { public WifiDevice createFromParcel(Parcel in) { WifiDevice device = new WifiDevice(); device.deviceAddress = in.readString(); device.deviceName = in.readString(); device.deviceState = in.readInt(); return device; } public WifiDevice[] newArray(int size) { return new WifiDevice[size]; } }; }<span style="font-family:Microsoft YaHei;font-size:12px;"> </span>
比起其他複雜的類來說,WifiDevice.java非常簡單,只有幾個成員變數和方法。 它代表一個接入到wifi熱點的客戶端的資訊,其中包括了客戶端的MAC地址,主機名和接入狀態。
一般情況下,我們需要的客戶端資訊也就是這幾個內容了。
不過有一個問題,WifiDevice只能表示已經正常接入熱點的客戶端,而那些曾經試圖連線熱點,但是並沒有連線成功的客戶端呢(比如由於密碼錯誤而連線失敗的)?如果我們
希望能得到周邊所有曾經和熱點發生過關係(!?)的客戶端的資訊呢?如何獲取這寫資訊呢?
先拋開以上的問題,我們先來了解一下,正常接入的客戶端是如何獲取它的資訊的。那先得看一下Tethering.java(\frameworks\base\services\core\java\com\android\server\connectivity\Tethering.java)
tether這個詞意思是拴繩,拴住的意思,這裡可以理解成是分享的意思,比如 WIFI_TETHERING(用WIFI分享網路), USB_TETHERING(用USB分享網路)。仔細閱讀Tethering中的程式碼,可以看到,這個類主要就是
為網路共享服務的,其中包括USB共享,藍芽共享和wifi共享。直接說Tethering,其實是跳過了connectivityservice的,上層獲取客戶端資訊,其實是通過ConnectivityManager呼叫ConnectivityService的方法,最終
呼叫到Tethering的 getTetherConnectedSta() 方法。
public List<WifiDevice> getTetherConnectedSta() { Iterator it; List<WifiDevice> TetherConnectedStaList = new ArrayList<WifiDevice>(); if (mContext.getResources().getBoolean(com.android.internal.R.bool.config_softap_extention)) { it = mConnectedDeviceMap.keySet().iterator(); while(it.hasNext()) { String key = (String)it.next(); WifiDevice device = (WifiDevice)mConnectedDeviceMap.get(key); if (VDBG) { Log.d(TAG, "getTetherConnectedSta: addr=" + key + " name=" + device.deviceName); } TetherConnectedStaList.add(device); } } return TetherConnectedStaList; }
仔細看下,無非是在Tethering中定義了一個mConnectedDeviceMap成員,專門用來存放接入的客戶端資訊,以下是定義
private HashMap<String, WifiDevice> mConnectedDeviceMap = new HashMap<String, WifiDevice>();
有兩個地方更新了這個HashMap。其實兩個地方都是 一樣的,一個是已經儲存這個裝置的IP資訊的,另一個是如果沒有儲存過,則先啟動一個DnsmasqThread執行緒,給客戶端分配IP後,
再講這個WifiDevice儲存在HashMap中。總體來說就是在第一個方法interfaceMessageRecevied(String message)中儲存了接入的客戶端資訊。
1.
private static class DnsmasqThread extends Thread {
private final Tethering mTethering;
private int mInterval;
private int mMaxTimes;
private WifiDevice mDevice;
public DnsmasqThread(Tethering tethering, WifiDevice device,
int interval, int maxTimes) {
super("Tethering");
mTethering = tethering;
mInterval = interval;
mMaxTimes = maxTimes;
mDevice = device;
}
public void run() {
boolean result = false;
try {
while (mMaxTimes > 0) {
result = mTethering.readDeviceInfoFromDnsmasq(mDevice);
if (result) {
if (DBG) Log.d(TAG, "Successfully poll device info for " + mDevice.deviceAddress);
break;
}
mMaxTimes --;
Thread.sleep(mInterval);
}
} catch (Exception ex) {
result = false;
Log.e(TAG, "Pulling " + mDevice.deviceAddress + "error" + ex);
}
if (!result) {
if (DBG) Log.d(TAG, "Pulling timeout, suppose STA uses static ip " + mDevice.deviceAddress);
}
// When STA uses static ip, device info will be unavaiable from dnsmasq,
// thus no matter the result is success or failure, we will broadcast the event.
// But if the device is not in L2 connected state, it means the hostapd connection is
// disconnected before dnsmasq get device info, so in this case, don't broadcast
// connection event.
WifiDevice other = mTethering.mL2ConnectedDeviceMap.get(mDevice.deviceAddress);
if (other != null && other.deviceState == WifiDevice.CONNECTED) {
mTethering.mConnectedDeviceMap.put(mDevice.deviceAddress, mDevice);
mTethering.sendTetherConnectStateChangedBroadcast();
} else {
if (DBG) Log.d(TAG, "Device " + mDevice.deviceAddress + "already disconnected, ignoring");
}
}
}
2.
public void interfaceMessageRecevied(String message) {
// if softap extension feature not enabled, do nothing
if (!mContext.getResources().getBoolean(com.android.internal.R.bool.config_softap_extention)) {
return;
}
if (DBG) Log.d(TAG, "interfaceMessageRecevied: message=" + message);
try {
WifiDevice device = new WifiDevice(message);
if (device.deviceState == WifiDevice.CONNECTED) {
mL2ConnectedDeviceMap.put(device.deviceAddress, device);
// When hostapd reported STA-connection event, it is possible that device
// info can't fetched from dnsmasq, then we start a thread to poll the
// device info, the thread will exit after device info avaiable.
// For static ip case, dnsmasq don't hold the device info, thus thread
// will exit after a timeout.
if (readDeviceInfoFromDnsmasq(device)) {
mConnectedDeviceMap.put(device.deviceAddress, device);
sendTetherConnectStateChangedBroadcast();
} else {
if (DBG) Log.d(TAG, "Starting poll device info for " + device.deviceAddress);
new DnsmasqThread(this, device,
DNSMASQ_POLLING_INTERVAL, DNSMASQ_POLLING_MAX_TIMES).start();
}
} else if (device.deviceState == WifiDevice.DISCONNECTED) {
mL2ConnectedDeviceMap.remove(device.deviceAddress);
mConnectedDeviceMap.remove(device.deviceAddress);
sendTetherConnectStateChangedBroadcast();
。。。。。。。interfaceMessageRecevied(String message)中,很明顯是入參message中攜帶了客戶端資訊,這個資訊由誰傳送,看一下它的呼叫關係。NetworkManagementService.java中的
notifyInterfaceMessage(String message)呼叫了它。
/**
* Notify our observers of a change in the data activity state of the interface
*/
private void notifyInterfaceMessage(String message) {
final int length = mObservers.beginBroadcast();
for (int i = 0; i < length; i++) {
try {
mObservers.getBroadcastItem(i).interfaceMessageRecevied(message);
} catch (RemoteException e) {
} catch (RuntimeException e) {
}
}
mObservers.finishBroadcast();
}
而它的上一級呼叫關係是在NetworkManagementService中的NetdCallbackReceiver接收到Event事件後,根據事件的型別來逐個處理的。
case NetdResponseCode.InterfaceMessage:
/*
* An message arrived in network interface.
* Format: "NNN IfaceMessage <3>AP-STA-CONNECTED 00:08:22:64:9d:84
*/
if (cooked.length < 3 || !cooked[1].equals("IfaceMessage")) {
throw new IllegalStateException(errorMessage);
}
Slog.d(TAG, "onEvent: "+ raw);
if(cooked[4] != null) {
notifyInterfaceMessage(cooked[3] + " " + cooked[4]);
} else {
notifyInterfaceMessage(cooked[3]);
}
return true;
// break;
看上面,是接收到InterfaceMessage訊息後,進行了訊息處理,訊息格式應該是註釋中的格式“NNN IfaceMessage <3>AP-STA-CONNECTED 00:08:22:64:9d:84”,
InterfaceMessage訊息又是從何而來呢?
這塊直接跳到HAL層了,Netd程序,不知道你是否有所瞭解,我之前也小小研究過一下,不過僅限於使用,因為之前做過Android系統的資料卡,啟動軟AP時,都是直接
使用Netd和Softap等命令了,很好用。不過 沒有更深入的瞭解,,後面有機會再好好學習一下。此處訊息的傳遞,就是,來源於softapcontroller。
void *SoftapController::threadStart(void *obj){
SoftapController *me = reinterpret_cast<SoftapController *>(obj);
struct wpa_ctrl *ctrl;
int count = 0;
ALOGD("SoftapController::threadStart...");
DIR *dir = NULL;
dir = opendir(HOSTAPD_SOCKETS_DIR);
if (NULL == dir && errno == ENOENT) {
mkdir(HOSTAPD_SOCKETS_DIR, S_IRWXU|S_IRWXG|S_IRWXO);
chown(HOSTAPD_SOCKETS_DIR, AID_WIFI, AID_WIFI);
chmod(HOSTAPD_SOCKETS_DIR, S_IRWXU|S_IRWXG);
} else {
if (dir != NULL) { /* Directory already exists */
ALOGD("%s already exists", HOSTAPD_SOCKETS_DIR);
closedir(dir);
}
if (errno == EACCES)
ALOGE("Cant open %s , check permissions ", HOSTAPD_SOCKETS_DIR);
}
chmod(HOSTAPD_DHCP_DIR, S_IRWXU|S_IRWXG|S_IRWXO);
ctrl = wpa_ctrl_open(HOSTAPD_UNIX_FILE);
while (ctrl == NULL) {
/*
* Try to connect to hostapd via wpa_ctrl interface.
* During conneciton process, it is possible that hostapd
* has station connected to it.
* Set sleep time to a appropriate value to lower the
* ratio that miss the STA-CONNECTED msg from hostapd
*/
usleep(20000);
ctrl = wpa_ctrl_open(HOSTAPD_UNIX_FILE);
if (ctrl != NULL || count >= 150) {
break;
}
count ++;
}
if (count == 150 && ctrl == NULL) {
ALOGE("Connection to hostapd Error.");
return NULL;
}
if (wpa_ctrl_attach(ctrl) != 0) {
wpa_ctrl_close(ctrl);
ALOGE("Attach to hostapd Error.");
return NULL;
}
while(me->mHostapdFlag) {
int res = 0;
char buf[256];
char dest_str[300];
while (wpa_ctrl_pending(ctrl)) {
size_t len = sizeof(buf) - 1;
res = wpa_ctrl_recv(ctrl, buf, &len);
if (res == 0) {
buf[len] = '\0';
ALOGD("Get event from hostapd (%s)", buf);
memset(dest_str, 0x0, sizeof(dest_str));
snprintf(dest_str, sizeof(dest_str), "IfaceMessage active %s", buf);
me->mSpsl->sendBroadcast(ResponseCode::InterfaceMessage, dest_str, false);
} else {
break;
}
}
if (res < 0) {
break;
}
sleep(2);
}
wpa_ctrl_detach(ctrl);
wpa_ctrl_close(ctrl);
return NULL;
}
此處是怎樣通訊的,我還沒有搞的徹底明白,不過並不影響我們理解整個流程,總是肯定是通過socket之類的方式,將InterfaceMessage訊息傳送出去,
訊息中所攜帶的字串儲存在"dest_str"中。如果你抓取一個logcat的log,就可以看到訊息字串的格式為“IfaceMessage active <3>AP-STA-CONNECTED 00:0a:f5:8a:be:58”,
可以和前面講的訊息格式對應上。
再往前追溯一下,softapcontroller的訊息來自於hostapd, (ps. hostapd的知識又是一個比較大的分支了,此處略去,可以把它理解成驅動和上層承上啟下的一個樞紐,它可以接收
來自wifi驅動的底層訊息,處理後分門別類的通知給上層)。其他的不多說,訊息是從sta_info.c中的ap_sta_set_authorized(struct hostapd_data *hapd, struct sta_info *sta, int authorized)
發出的。這裡面主要是傳送connect和disconnect訊息的,如果sta->flags & WLAN_STA_AUTHORIZED,就是說如果鑑權過了話,就傳送connect, 沒有過就傳送disconnect訊息。
void ap_sta_set_authorized(struct hostapd_data *hapd, struct sta_info *sta,
int authorized)
{
const u8 *dev_addr = NULL;
char buf[100];
#ifdef CONFIG_P2P
u8 addr[ETH_ALEN];
#endif /* CONFIG_P2P */
if (!!authorized == !!(sta->flags & WLAN_STA_AUTHORIZED))
return;
#ifdef CONFIG_P2P
if (hapd->p2p_group == NULL) {
if (sta->p2p_ie != NULL &&
p2p_parse_dev_addr_in_p2p_ie(sta->p2p_ie, addr) == 0)
dev_addr = addr;
} else
dev_addr = p2p_group_get_dev_addr(hapd->p2p_group, sta->addr);
#endif /* CONFIG_P2P */
if (dev_addr)
os_snprintf(buf, sizeof(buf), MACSTR " p2p_dev_addr=" MACSTR,
MAC2STR(sta->addr), MAC2STR(dev_addr));
else
os_snprintf(buf, sizeof(buf), MACSTR, MAC2STR(sta->addr));
if (authorized) {
wpa_msg(hapd->msg_ctx, MSG_INFO, AP_STA_CONNECTED "%s", buf);
if (hapd->msg_ctx_parent &&
hapd->msg_ctx_parent != hapd->msg_ctx)
wpa_msg_no_global(hapd->msg_ctx_parent, MSG_INFO,
AP_STA_CONNECTED "%s", buf);
sta->flags |= WLAN_STA_AUTHORIZED;
} else {
wpa_msg(hapd->msg_ctx, MSG_INFO, AP_STA_DISCONNECTED "%s", buf);
if (hapd->msg_ctx_parent &&
hapd->msg_ctx_parent != hapd->msg_ctx)
wpa_msg_no_global(hapd->msg_ctx_parent, MSG_INFO,
AP_STA_DISCONNECTED "%s", buf);
sta->flags &= ~WLAN_STA_AUTHORIZED;
}
if (hapd->sta_authorized_cb)
hapd->sta_authorized_cb(hapd->sta_authorized_cb_ctx,
sta->addr, authorized, dev_addr);
}
一開始只知道此處會有訊息傳送,但是不知道是怎樣傳送了,仔細看了一下wpa_msg()函式的原型,才搞明白了。
void wpa_msg(void *ctx, int level, const char *fmt, ...)
{
va_list ap;
char *buf;
int buflen;
int len;
char prefix[130];
va_start(ap, fmt);
buflen = vsnprintf(NULL, 0, fmt, ap) + 1;
va_end(ap);
buf = os_malloc(buflen);
if (buf == NULL) {
wpa_printf(MSG_ERROR, "wpa_msg: Failed to allocate message "
"buffer");
return;
}
wpa_printf(MSG_DEBUG, "@@@@wpa_msg");
va_start(ap, fmt);
prefix[0] = '\0';
if (wpa_msg_ifname_cb) {
const char *ifname = wpa_msg_ifname_cb(ctx);
if (ifname) {
int res = os_snprintf(prefix, sizeof(prefix), "%s: ",
ifname);
if (res < 0 || res >= (int) sizeof(prefix))
prefix[0] = '\0';
}
}
len = vsnprintf(buf, buflen, fmt, ap);
va_end(ap);
wpa_printf(level, "%s%s", prefix, buf);
if (wpa_msg_cb){
wpa_printf(MSG_DEBUG, "@@@@wpa_msg_cb");
wpa_msg_cb(ctx, level, 0, buf, len);
}
os_free(buf);
}
注意後面全域性變數wpa_msg_cb, 在hostapd_ctrl_iface_init(struct hostapd_data *hapd)中,初始化的時候,可以看到這麼一行程式碼:
hapd->msg_ctx = hapd;
wpa_msg_register_cb(hostapd_ctrl_iface_msg_cb);
這裡註冊了一個回撥hostapd_ctrl_iface_msg_cb(void *ctx, int level, int global, const char *txt, size_t len),這函式呼叫了hostapd_ctrl_iface_send(.....)。
而這個和wpa_msg_cb有什麼關係呢?那你就得看看wpa_msg_register_cb了,原來它是將註冊的回撥函式指標賦給了wpa_msg_cb,那麼wpa_msg_cb相當於
最終呼叫了hostapd_ctrl_iface_send用於傳送訊息啦。
void wpa_msg_register_cb(wpa_msg_cb_func func)
{
wpa_msg_cb = func;
}
以上就是整個framework接收到接入客戶端連線成功,並且獲取客戶端資訊的流程,不過我原本看的時候,是從下往上看的,先看了hostapd傳送訊息的部分,再看的上層。