實戰Android Wifi P2p

在咱們的應用設計中,有這麼一個需求,將一臺已鏈接無人機的Android手機(主機)的圖傳發送給另外一臺手機(從機),而且從機也能夠控制主機的一些操做,以此達到無人機協做的目的。發送數據咱們能夠經過socket來實現,但前提是從機或是主機如何知道對方的IP和端口呢?java

Wifi P2P

Android有一種鏈接方式叫 Wi-Fi點對點(P2P),他不須要組織局域網環境,在手機兩端打開wifi就能夠搜索到對方,主機經過註冊服務的方式,將本身的IP和端口以參數攜帶的方式暴露出去,從機經過搜索服務的方式搜索周邊的服務,將搜索到的服務進行解析對比取出IP和端口值,從機最終經過socket往這個解析成功的IP和端口發送數據。android

目的

在接下來進行的一切操做中,咱們要達到的目的有兩個:ios

  • 獲取拓展參數
  • 解析拿到IP

註冊服務

wifip2p服務註冊須要幾個主要的參數:git

  • serviceName : 服務的名稱
  • serviceType : 服務類型,命名格式爲 _<protocol>._<transportlayer>
  • txtMap : 拓展參數 服務名稱是咱們在從機搜索時匹配對方的依據;serviceType是服務的一種類型,好比咱們接觸最多的有打印機服務 _ipp._tcp ;txtMap是一個字典型的數據,他能夠隨註冊服務一塊暴露出去,好比主機開啓了三個socket server,咱們須要將這三個socket server的端口告知從機,就能夠採用拓展參數的方式。

構建服務

mManager = (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE);
mChannel = mManager.initialize(context, context.getMainLooper(), null);
//模擬主機的圖傳端口是11021
map.put("image_port","11021");
p2pDnsSdServiceInfo = WifiP2pDnsSdServiceInfo.newInstance(serviceName, serviceType, map);
複製代碼

啓動服務

//添加服務 
mManager.addLocalService(mChannel, p2pDnsSdServiceInfo,listener);
//啓動服務
mManager.discoverPeers(mChannel, null);
複製代碼

搜索服務

搜索服務的邏輯會比較有點複雜,他須要配合BroadCastReceiver一塊來實現github

初始化廣播監聽

mManager = (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE);
mChannel = mManager.initialize(context, context.getMainLooper(), null);
broadCastReceiver = new WifiBroadCastReceiver(mManager, mChannel, this);
context.registerReceiver(broadCastReceiver, intentFilter);
複製代碼

廣播會實時監聽當前的WifiP2p網絡狀態是否已鏈接,若是是鏈接狀態的話,則直接返回鏈接的結果信息,也就是返回搜索到的服務的IP,這個地方有一個注意點,後面再說網絡

WifiBroadCastReceiver.javaapp

class WifiBroadCastReceiver extends BroadcastReceiver {
        WifiP2pManager mManager;
        WifiP2pManager.Channel mChannel;
        WifiP2pManager.ConnectionInfoListener listener;

        public WifiBroadCastReceiver(WifiP2pManager mManager, WifiP2pManager.Channel mChannel, WifiP2pManager.ConnectionInfoListener listener) {
            this.listener = listener;
            this.mChannel = mChannel;
            this.mManager = mManager;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(intent.getAction())) {
                if (mManager == null) {
                    return;
                }
                NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
                if (networkInfo.isConnected()) {
                    //注意此請求,後面再講解
                    mManager.requestConnectionInfo(mChannel, new WifiP2pManager.ConnectionInfoListener);
                }
            }
        }
    }
複製代碼

添加搜索監聽,獲取拓展參數

WifiP2pManager.DnsSdTxtRecordListener txtListener = new WifiP2pManager.DnsSdTxtRecordListener() {
     @Override
     public void onDnsSdTxtRecordAvailable(String fullDomain, Map record, WifiP2pDevice device) {
         //record 就是服務端發送出去的拓展參數
        //fullDomain 服務端的serviceName+".local"
        //device 拿到服務的一些device信息,能夠拿到mac地址 device.deviceAddress
     }
 };
//設置監聽
mManager.setDnsSdResponseListeners(mChannel, null, txtListener);
//添加到服務
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
mManager.addServiceRequest(mChannel, serviceRequest, new WifiP2pManager.ActionListener());
//開啓搜索
mManager.discoverServices(mChannel, new WifiP2pManager.ActionListener());
複製代碼

在wifip2p發起搜索的時候,若是搜索到對方會觸發 WifiP2pManager.DnsSdTxtRecordListener 監聽,但這僅僅只是一個搜索到對方的過程,而且在該回調中是拿不到真正的服務端IP值的,此回調只能拿到拓展參數和服務端的物理設備信息less

請求服務端鏈接

在搜索端發現服務的時候,接下來就是一個請求的過程,在 WifiP2pManager.DnsSdTxtRecordListener 監聽中發起connect鏈接,這個過程就是請求但願本身與服務端創建鏈接,服務端會收到一個由系統彈出的dialog,是否贊成客戶端鏈接socket

//將搜索到的服務的mac地址添加到配置裏面,以備後面對該地址發起鏈接操做  
WifiP2pConfig config = new WifiP2pConfig();
     config.deviceAddress = device.deviceAddress;
     config.groupOwnerIntent = 0;

  if (serviceRequest != null){
    //移除服務
     mManager.removeServiceRequest(mChannel, serviceRequest,null);
  }
  //請求創建鏈接
  mManager.connect(mChannel, config, new WifiP2pManager.ActionListener() {
            @Override
            public void onSuccess() {
                LogUtils.log("P2PManager connect onSuccess ");
            }

            @Override
            public void onFailure(int errorCode) {
                LogUtils.log("P2PManager connect onFailure errorCode=" + errorCode);
            }
        });
複製代碼

解析IP

當服務端選擇贊成的時候,至關因而激活了WifiP2pManager的鏈接,會觸發在上面註冊的廣播,networkInfo.isConnected 就會返回 true ,而後開啓 mManager.requestConnectionInfo(mChannel,new WifiP2pManager.ConnectionInfoListener); 的請求,觸發 onConnectInfoAvailable 方法tcp

WifiP2pManager.ConnectionInfoListener.java

@Override
 public void onConnectionInfoAvailable(WifiP2pInfo info) {
        try {
            //todo 獲取服務端IP地址
           InetAddress.getByName(info.groupOwnerAddress.getHostAddress())
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

結論

從上面的一系列過程當中,咱們能夠整理出一個流程出來,服務端的註冊仍是比較簡單的,咱們來整理下搜索端:

  • 初始化搜索監聽
  • 開啓搜索
  • 回調搜索監聽
  • 請求創建鏈接
  • 解析服務IP

看似流程明白了,但在咱們實踐的過程當中,這個流程是會發生微妙的變化的,在咱們的講解中,是以一個徹底沒有創建過鏈接的設備來闡述的,假設一種狀況,在我進行了上面的一波操做後,咱們又進行了一次搜索對方的操做,你們會以爲這樣的流程會是怎樣的呢?他就會發生:

  • 初始化搜索監聽
  • 開啓搜索
  • 解析服務IP
  • 回調搜索監聽

有沒有發現 請求創建鏈接 的過程沒有了,並且在開啓搜索以後,先返回的解析服務IP,而後 回調搜索監聽 拿到拓展參數值,這是什麼緣由形成的呢?最主要的緣由是在咱們第一次創建鏈接的時候,服務端和搜索端就已經完成了鏈接的操做,在第二次搜索時廣播監聽到 WifiP2pManagernetworkInfo,isConnected()true ,因此就先發起了 解析服務IP 的操做,因此,回調搜索監聽 就會晚一點達到。

在咱們以前的業務中,最早是在 回調搜索監聽 中先拿到拓展參數,而後設置到全局,最後在 解析服務IP 中拿到IP地址,而且將這個全局的拓展參數一併返回,而後再實踐中發現了上面闡述的問題,後來,咱們是這麼解決的:

最終回調

InetAddress inetAddress;
  public void callbackSuccess(InetAddress inetAddress) {
        //存儲有效地址到全局
        if (inetAddress != null) {
            this.inetAddress = inetAddress;
        }
        //判斷拓展參數和地址是否都有值
        if (p2pServices != null && p2pServices.size() > 0 && inetAddress != null) {
           //返回結果 
           WifiP2pClient.this.clientCallBack.onSuccess(inetAddress, p2pServices);
        }
    }
複製代碼

回調搜索監聽

WifiP2pManager.DnsSdTxtRecordListener txtListener = new WifiP2pManager.DnsSdTxtRecordListener() {
  @Override
  public void onDnsSdTxtRecordAvailable(String fullDomain, Map record, WifiP2pDevice device) {
      p2pServices.clear();
      p2pServices.addAll(record);
      callbackSuccess(null);
   }
}
複製代碼

解析服務IP

@Override
 public void onConnectionInfoAvailable(WifiP2pInfo info) {
     callbackSuccess(InetAddress.getByName(info.groupOwnerAddress.getHostAddress()));
 }
複製代碼

因爲 回調搜索監聽解析服務IP 兩個操做都是不固定的,因此,採用了全局設置有效參數來解決問題。

注意

  • wifi p2p 獲取\設置拓展參數必須在API 21以上
  • wifi p2p 的serviceName不能爲中文
  • wifi p2p 的serviceType 格式爲 _<protocol>._<transportlayer> ,千萬不要在最後加 .
  • wifi p2p 二次鏈接先返回的解析IP,後觸發參數解析

你覺得就這麼結束了嗎?No,業務場景繼續升級,咱們須要實現跨平臺操做,實現Android與iOS的互通,接下來,又要進入另外一個話題 NsdManager

Nsd(network service discovery)

Wi-Fi NSD官方介紹

Network service discovery (NSD) gives your app access to services that other devices provide on a local network 複製代碼

正如官往介紹,NSD要想實現兩端手機的通訊必須是在一個局域網環境下才能搜索到對方。NSD方式顯然沒有wifip2p那麼便捷,須要本身去構建一個局域網,局域網環境能夠經過一臺設備開啓熱點,讓另外一臺設備鏈接。NSD還有一個過人之處,那就是跨平臺,它能夠搜索到iOS設備暴露出去服務,拿到對方的IP和端口,github有一份示例 demo,能夠先從它入手學習。

目的

在接下來進行的一切操做中,咱們要達到的目的有兩個:

  • 獲取拓展參數
  • 解析拿到IP
  • 解析拿到port

註冊服務

Nsd註冊服務和wifiP2p差很少:

  • serviceName
  • serviceType
  • setPort 設置端口
  • setAttribute 設置拓展參數 Nsd參數設置會比wifiP2p多一個設置端口的功能,咱們在上面講解wifiP2p將socket server的端口暴露出去時,採用的是拓展參數的形式,但這個地方是有限制的,就是在API 21如下,拓展參數的獲取和設置是沒有用的,在Nsd上面也是如此,因此,Nsd在系統兼容方面多了一個選擇和保障。

構建服務

mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(serviceName);
serviceInfo.setServiceType(serviceType);
//若是要設置端口的話,該值必須大於0
serviceInfo.setPort(port);//port must >0
//設置拓展參數
if (map != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
     for (Map.Entry<String, String> m : map.entrySet()) {
                serviceInfo.setAttribute(m.getKey(), m.getValue());
     }
}
複製代碼

啓動服務

mRegistrationListener = new NsdManager.RegistrationListener() {
            @Override
            public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {}

            @Override
            public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}

            @Override
            public void onServiceUnregistered(NsdServiceInfo arg0) {}

            @Override
            public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
        }; 
//註冊服務
mNsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
複製代碼

搜索服務

Nsd的搜索相對於wifiP2p來講十分的簡單,他不須要wifip2p創建鏈接的過程,對方在暴露出服務時,搜索端搜索到對方時能夠直接拿到對方的IP、端口和拓展參數,因此十分的方便

開啓搜索監聽

private NsdManager.DiscoveryListener nsDicListener = new NsdManager.DiscoveryListener() {
        @Override
        public void onDiscoveryStarted(String serviceType) {}

        @Override
        public void onStopDiscoveryFailed(String serviceType, int errorCode) {}

        @Override
        public void onStartDiscoveryFailed(String serviceType, int errorCode) {}

        @Override
        public void onServiceLost(NsdServiceInfo serviceInfo) { }

        @Override
        public void onServiceFound(NsdServiceInfo serviceInfo) {
            //判斷搜索到的服務名稱是否匹配服務端配置的名稱
            if (serviceName.equals(serviceInfo.getServiceName())) {
                //開啓解析服務
                resolveNsd(serviceInfo);
            }
        }
        @Override
        public void onDiscoveryStopped(String serviceType) {}
    };

mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, nsDicListener);
複製代碼

解析服務

private void resolveNsd(NsdServiceInfo serviceInfo) {
    mNsdManager.resolveService(serviceInfo, new NsdManager.ResolveListener() {
        @Override
        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {}

        @Override
        public void onServiceResolved(NsdServiceInfo nsdServiceInfo) {
                HashMap<String, String> serviceMap = new HashMap<>();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    //獲取拓展參數
                    Map<String, byte[]> map = nsdServiceInfo.getAttributes();
                    for (Map.Entry<String, byte[]> m : map.entrySet()) {
                        serviceMap.put(m.getKey(), new String(m.getValue(), 0, m.getValue().length));
                    }
                }
                //成功回調結果
                WifiNsdClient.this.clientCallBack.onSuccess(nsdServiceInfo.getServiceName(), nsdServiceInfo.getHost(), nsdServiceInfo.getPort(), serviceMap);
            }
        });
    }
複製代碼

結論

Nsd的整個過程並不難,過程也很是的簡單,他沒有wifiP2p混亂的步驟,也沒有廣播參與,也沒有創建鏈接的過程,惟一缺點就是須要自建局域網,Nsd搜索流程爲:

  • 開啓搜索
  • 解析服務拿到端口、ip、拓展參數

固然,在實踐過程當中,也發現了Nsd的弊端,在咱們的業務中,有可能會有兩個飛手,他們都在一個局域網中,而且他們都開啓了兩個服務等待從機進行鏈接,從機在搜索的時候確定會發現兩個服務,而後對這兩個服務進行解析,可是,咱們發現,在第一個服務解析時返回的都是成功的,第二次解析時永遠都是失敗的,而後咱們根據返回的 errorCode 進行源碼跟蹤,跟蹤到返回的內容是 Indicates that the operation failed beacause it is already active , 意思就是當前Nsd解析時處於激活的狀態,因此操做失敗。根據這段內容咱們找到了源碼的出錯位置

NsdService

... 
case NsdManager.RESOLVE_SERVICE:
    if (DBG) Slog.d(TAG, "Resolve service");
    servInfo = (NsdServiceInfo) msg.obj;
    clientInfo = mClients.get(msg.replyTo);
    //若是mResolvedService不爲空,則直接拋出錯誤
    if (clientInfo.mResolvedService != null) {
        replyToMessage(msg, NsdManager.RESOLVE_SERVICE_FAILED,
                NsdManager.FAILURE_ALREADY_ACTIVE);
        break;
    }
     id = getUniqueId();
     //解析服務操做
     if (resolveService(id, servInfo)) {
        //建立mResolvedService
        clientInfo.mResolvedService = new NsdServiceInfo();
        storeRequestMap(msg.arg2, id, clientInfo, msg.what);
     } else {
        replyToMessage(msg, NsdManager.RESOLVE_SERVICE_FAILED,
                NsdManager.FAILURE_INTERNAL_ERROR);
     }
...
複製代碼

從源碼中能夠看到,在第一次解析服務時,clientInfo.mResolveService 爲空,因此後面就會開始建立 mResolvedService ,而後進行解析,若是這時候第二個服務進來了,clientInfo.mResolveService 確定是不爲空的,因此,就會調用 replyToMessage 方法,觸發咱們剛剛接收到的錯誤信息。

但也不是說Nsd不能解析多個服務,只是在解析一個服務時是一個耗時的任務,但搜索服務是很是快速的,咱們必需要等一個服務解析完成時,才能夠進行下一個解析,源碼以下:

case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS:
    /* NNN resolveId hostname ttl addr */
    try {
        clientInfo.mResolvedService.setHost(InetAddress.getByName(cooked[4]));
        clientInfo.mChannel.sendMessage(NsdManager.RESOLVE_SERVICE_SUCCEEDED,
               0, clientId, clientInfo.mResolvedService);
    } catch (java.net.UnknownHostException e) {
        clientInfo.mChannel.sendMessage(NsdManager.RESOLVE_SERVICE_FAILED,
                NsdManager.FAILURE_INTERNAL_ERROR, clientId);
    }
    stopGetAddrInfo(id);
    removeRequestMap(clientId, id, clientInfo);
    //重置爲null
    clientInfo.mResolvedService = null;
    break;
複製代碼

在解析成功的回調中,最後會把 mResolveService 重置爲null,這樣再次解析的話,就不會拋出錯誤信息。

因爲屢次解析服務會產生問題,因此,咱們要保證搜索端搜索到的服務是惟一肯定的,這樣就能夠避免多服務解析的問題,最終咱們給的解決方案是從serviceName中入手,在Nsd中,serviceName的做用並無那麼大,咱們徹底能夠利用他來達到傳參的目的,咱們產品設計是主機展現二維碼內容,從機掃碼進行鏈接,二維碼內容是一串隨機碼加平臺信息,隨機碼的主要目的是爲了區別不一樣Master服務,而後Master將這個二維碼內容設置到Nsd的serviceName中,而後暴露服務,從機掃碼拿到這個二維碼內容,而後比對Nsd搜索到的serviceName是否與從機掃到的二維碼內容一致,是的話,就直接解析。

注意

  • Nsd 不能搜索多個知足條件的服務,Nsd服務解析一次只容許解析一個服務,下個服務的解析必須等當前解析完成才能解析
  • Nsd設置端口必須大於0
  • Nsd 獲取\設置拓展參數必須在API 21以上

總結

無人機開發是有趣的,但也是充滿各類挑戰的,好比主機同步視頻給從機,如何給一幀數據分段,怎麼分穩定,從機接收時如何拼接完整的一幀數據顯示。最後,也能夠體驗下咱們的產品 Mesh Lite

最後給出一份WifiManager源碼

相關文章
相關標籤/搜索