实战Android Wifi P2p

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 实战Android Wifi P2p

在我们的应用设计中,有这么一个需求,将一台已连接无人机的Android手机(主机)的图传发送给另一台手机(从机),并且从机也可以控制主机的一些操作,以此达到无人机协作的目的。发送数据我们可以通过socket来实现,但前提是从机或是主机如何知道对方的IP和端口呢?


Wifi P2P


Android有一种连接方式叫 Wi-Fi点对点(P2P),他不需要组织局域网环境,在手机两端打开wifi就可以搜索到对方,主机通过注册服务的方式,将自己的IP和端口以参数携带的方式暴露出去,从机通过搜索服务的方式搜索周边的服务,将搜索到的服务进行解析对比取出IP和端口值,从机最终通过socket往这个解析成功的IP和端口发送数据。


目的


在接下来进行的一切操作中,我们要达到的目的有两个:


  • 获取拓展参数
  • 解析拿到IP


注册服务


wifip2p服务注册需要几个主要的参数:


  • 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一块来实现


初始化广播监听


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.java

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值的,此回调只能拿到拓展参数和服务端的物理设备信息


请求服务端连接


在搜索端发现服务的时候,接下来就是一个请求的过程,在 WifiP2pManager.DnsSdTxtRecordListener 监听中发起connect连接,这个过程就是请求希望自己与服务端建立连接,服务端会收到一个由系统弹出的dialog,是否同意客户端连接


//将搜索到的服务的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 方法


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

目录
相关文章
|
3月前
|
安全 Android开发 Kotlin
Android经典实战之SurfaceView原理和实践
本文介绍了 `SurfaceView` 这一强大的 UI 组件,尤其适合高性能绘制任务,如视频播放和游戏。文章详细讲解了 `SurfaceView` 的原理、与 `Surface` 类的关系及其实现示例,并强调了使用时需注意的线程安全、生命周期管理和性能优化等问题。
176 8
|
2月前
|
缓存 前端开发 Android开发
Android实战之如何截取Activity或者Fragment的内容?
本文首发于公众号“AntDream”,介绍了如何在Android中截取Activity或Fragment的屏幕内容并保存为图片。包括截取整个Activity、特定控件或区域的方法,以及处理包含RecyclerView的复杂情况。
26 3
|
3月前
|
Android开发 开发者 索引
Android实战经验之如何使用DiffUtil提升RecyclerView的刷新性能
本文介绍如何使用 `DiffUtil` 实现 `RecyclerView` 数据集的高效更新,避免不必要的全局刷新,尤其适用于处理大量数据场景。通过定义 `DiffUtil.Callback`、计算差异并应用到适配器,可以显著提升性能。同时,文章还列举了常见错误及原因,帮助开发者避免陷阱。
261 9
|
3月前
|
开发工具 Android开发 git
Android实战之组件化中如何进行版本控制和依赖管理
本文介绍了 Git Submodules 的功能及其在组件化开发中的应用。Submodules 允许将一个 Git 仓库作为另一个仓库的子目录,有助于保持模块独立、代码重用和版本控制。虽然存在一些缺点,如增加复杂性和初始化时间,但通过最佳实践可以有效利用其优势。
44 3
|
2月前
|
Android开发
Android实战之如何快速实现自动轮播图
本文介绍了在 Android 中使用 `ViewPager2` 和自定义适配器实现轮播图的方法,包括添加依赖、布局配置、创建适配器及实现自动轮播等步骤。
86 0
|
3月前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
在Android开发中,为应对复杂应用场景和繁重计算任务,多线程与异步编程成为保证UI流畅性的关键。本文将介绍Android中的多线程基础,包括Thread、Handler、Looper、AsyncTask及ExecutorService等,并通过示例代码展示其实用性。AsyncTask适用于简单后台操作,而ExecutorService则能更好地管理复杂并发任务。合理运用这些技术,可显著提升应用性能和用户体验,避免内存泄漏和线程安全问题,确保UI更新顺畅。
121 5
|
2月前
|
Android开发
Android开发显示头部Bar的需求解决方案--Android应用实战
Android开发显示头部Bar的需求解决方案--Android应用实战
24 0
|
3月前
|
编解码 前端开发 Android开发
Android经典实战之TextureView原理和高级用法
本文介绍了 `TextureView` 的原理和特点,包括其硬件加速渲染的优势及与其他视图叠加使用的灵活性,并提供了视频播放和自定义绘制的示例代码。通过合理管理生命周期和资源,`TextureView` 可实现高效流畅的图形和视频渲染。
288 12
|
3月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
164 1
|
3月前
|
Android开发 容器
Android经典实战之如何获取View和ViewGroup的中心点
本文介绍了在Android中如何获取`View`和`ViewGroup`的中心点坐标,包括计算相对坐标和屏幕上的绝对坐标,并提供了示例代码。特别注意在视图未完成测量时可能出现的宽高为0的问题及解决方案。
61 7