实战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源码

相关文章
相关标签/搜索