最近在看AR相关的东西, 其中一个ARWorldMap使用到的Multipeer的技术感觉棒极了。简单就是说, 不需要通过远程的服务器,只在局域网内通过wifi、蓝牙建立起来的通信,有点类似于Apple设备上的AirDrop。尽管对目前强连接的网络游戏环境格格不入(特别是5G大环境下),不过在我看来,对于实时的每帧更新的大批量数据、多用户连接的通信场景,直连技术对缓解服务器压力有不可替代的作用。

在AR中,对环境生成特征点发送在同一局域网的设备,这样另一台设备接到发送到的数据,这样就可以判断两台设备的相对关系,进一步交互操作。两台设备之间没有第三方的服务器来桥接,就可以自主快速的通信。我看在unity官方的例子中, 针对ios端MultipeerConnectivity进行封装,使之能在unity编辑器中使用c#调用其接口。


什么是Multipeer Connectivity

在iOS7中,引入了一个全新的框架——Multipeer Connectivity(多点连接)。利用Multipeer Connectivity框架,即使在没有连接到WiFi(WLAN)或移动网络(xG)的情况下,距离较近的Apple设备(iMac/iPad/iPhone)之间可基于蓝牙和WiFi(P2P WiFi)技术进行发现和连接实现近场通信。

相比AirDrop,Multipeer Connectivity在进行发现和会话时并不要求同时打开WiFi和蓝牙,也不像AirDrop那样强制打开这两个开关,而是根据条件适时选择使用蓝牙或(和)WiFi。

MultipeerConnectivity.framework

以下是MultipeerConnectivity.framework的四个核心对象:

  • Peer ID’s allow for unique identification.
  • Advertiser objects tells others they’re available.
  • Browser objects browse for advertised devices.
  • Session objects handle the communications.

@class MCPeerID

MCPeerID represents a peer in a multipeer session.

Peer IDs (MCPeerID) uniquely identify an app running on a device to nearby peers.

provide information that identifies the device and its user to other nearby devices.

类似sockaddr,用于标识连接的两端endpoint,通常是昵称或设备名称。

该对象只开放了displayName属性,私有MCPeerIDInternal对象持有的设备相关的_idString/_pid64字段并未公开。

在许多情况下,客户端同时广播并发现同一个服务,这将导致一些混乱,尤其是在client/server模式中。所以,每一个服务都应有一个类型标示符——serviceType,它是由ASCII字母、数字和“-”组成的短文本串,最多15个字符。

@class MCNearbyServiceAdvertiser

MCNearbyServiceAdvertiser advertises availability of the local peer, and handles invitations from nearby peers.

类似broadcaster。

主线程(com.apple.main-thread(serial))创建MCNearbyServiceAdvertiser并启动startAdvertisingPeer。
MCNearbyServiceAdvertiserDelegate异步回调(didReceiveInvitationFromPeer)切换回主线程。
在主线程didReceiveInvitationFromPeer中创建MCSession并invitationHandler(YES, session)接受会话连接请求(accept参数为YES)。

@class MCNearbyServiceBrowser

MCNearbyServiceBrowser looks for nearby peers, and connects them to sessions.

类似servo listen+client connect。

主线程(com.apple.main-thread(serial))创建MCNearbyServiceBrowser并启动startBrowsingForPeers。
MCNearbyServiceBrowserDelegate异步回调(foundPeer/lostPeer)切换回主线程。
主线程创建MCSession并启动invitePeer。
 

@class MCSession

A MCSession facilitates communication among all peers in a multipeer session.

(MCSession) provide support for communication between connected peer devices(identified by MCPeerID). 

Session objects maintain a set of peer ID objects that represent the peers connected to the session. 

注意,peerID并不具备设备识别属性。

类似TCP链接中的socket。创建MCSession时,需指定自身MCPeerID,类似bind。

为避免频繁的会话数据通知阻塞主线程,MCSessionDelegate异步回调(didChangeState/didReceiveCertificate/didReceiveData/didReceiveStream)有一个专门的回调线程——com.apple.MCSession.callbackQueue(serial)。为避免阻塞MCSeesion回调线程,最好新建数据读(写)线程!

Android Wi-Fi Direct

类似于ios平台的Multipeer Connectivity, Android用Wi-Fi Direct技术可以让具备硬件支持的设备在没有中间接入点的情况下进行直接互联。Android 4.0(API版本14)及以后的系统都提供了对Wi-Fi Direct的API支持。通过对这些API的使用,开发者可以实现支持Wi-Fi Direct的设备间进行相互探测和连接,从而获得较之蓝牙更远距离的高速数据通信效果。

关于Wi-Fi Direct的API函数的使用需要注意一下几个要点:

  • 用于探测(discover)对等设备(peers)、向对等设备发起请求(request)以及建立连接(connect)的方法定义在类WifiP2pManager中。
  • 通过设置监听器(Listener)可以获知WifiP2pManager中方法调用的成功与否。监听器以参数的形式传递给被调用的方法。
  • 当发现新对等设备或链接丢失的时候,Wi-Fi Direct系统(framework)以意向(Intent)的方式根据检测到的不同事件做出相应的通知。

开发中,以上三点的配合使用相当普遍。简单举个例子,定义一个监听器WifiP2pManager.ActionListener并调用函数discoverPeers(),当相应事件发生的时候就会在ActionListener.onSuccess()和ActionListener.onFailure()两个方法中得到通知。当discoverPeers()方法检测到了对等设备列表变化的时候,可以收到由系统广播(broadcast)发出一个WIFI_P2P_PEERS_CHANGED_ACTION意向。

WifiP2pManager类所提供的方法可用于操作当前设备中的Wi-Fi硬件,实现诸如探测对、连接对等设备等功能。目前所支持的功能如下:

Method Description
initialize() 通过Wi-Fi框架对应用来进行注册。这个方法必须在任何其他Wi-Fi直连方法使用之前调用
connect() 开始一个拥有特定设置的设备的点对点连接
cancelConnect() 取消任何一个正在进行的点对点组的连接
requestConnectInfo() 获取一个设备的连接信息
createGroup() 以当前设备为组拥有者来创建一个点对点连接组
removeGroup() 移除当前的点对点连接组
requestGroupInfo() 获取点对点连接组的信息
discoverPeers() 初始化对等设备的发现
requestPeers() 获取当前发现的对等设备列表

WifiP2pManager中所提供的方法允许特定的监听器作为参数传入,以便Wi-Fi Direct机制能够汇报函数调用的结果。下表中列出了目前支持的监听器接口以及WifiP2pManager中用到相应监听器的方法。

</tr>
Listener interface Associated actions
WifiP2pManager.ActionListener connect(),cancelConnect(),createGroup(),removeGroup(), and discoverPeers()
WifiP2pManager.ChannelListener initialize()
WifiP2pManager.ConnectionInfoListener requestConnectInfo()
WifiP2pManager.GroupInfoListener requestGroupInfo()
WifiP2pManager.PeerListListener requestPeers()

每当有Wi-Fi Direct事件发生的时候(例如,发现新的对等设备、设备的Wi-Fi状态改变等),Wi-Fi Direct API会以广播的形式发出一个意向。而在应用程序中需要做的事情就是创建广播接收器(creating a broadcast receiver)来处理这些意向:

Intent Description
WIFI_P2P_CONNECTION_CHANGED_ACTION 当设备的Wi-Fi连接信息状态改变时候进行广播。
WIFI_P2P_PEERS_CHANGED_ACTION 当调用discoverPeers()方法的时候进行广播。在你的应用里处理此意图时,你通常会调用requestPeers()去获得对等设备列表的更新
WIFI_P2P_STATE_CHANGED_ACTION 当设备的Wi-Fi 直连功能打开或关闭时进行广播
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 当设备的详细信息改变的时候进行广播,比如设备的名称

创建广播接收器以处理Wi-Fi Direct意向(Creating a Broadcast Receiver for Wi-Fi Direct Intents)

广播接收器可以让应用程序接收到Android系统所发出的广播意向。这样,应用程序就能对感兴趣的事件做出响应。创建广播接收器的基本步骤如下:

创建一个继承BroadcastReceiver类的新类。构造函数的参数分别传递WifiP2pManager,WifiP2pManager.Channel,以及在这个广播接收器中需要注册的活动(activity)。这是一种最常见的参数设置模式,它让广播接收器能够引起活动作出更新,同时又能在必要时使用Wi-Fi硬件和通信信道。
在广播接收器的onReceive()函数中,针对感兴趣的特定意向可以执行相应的动作(actions)。例如,当广播接收器收到了意向WIFI_P2P_PEERS_CHANGED_ACTION,就可以调用requestPeers()方法来列举出当前探测到的对等设备。
下面的代码将展示了如何创建一个特定的广播接收器。例子中的广播接收器以WifiP2pManager对象和一个活动(activity)作为参数,并使用它们针对收到的意向(intent)做出相应的动作(action):

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {
    private WifiP2pManager manager;
    private Channel channel;
    private MyWiFiActivity activity;
 
    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
            MyWifiActivity activity) {
        super();
        this.manager = manager;
        this.channel = channel;
        this.activity = activity;
    }
 
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
 
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Check to see if Wi-Fi is enabled and notify appropriate activity
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
            // Call WifiP2pManager.requestPeers() to get a list of current peers
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // Respond to new connection or disconnections
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            // Respond to this device's wifi state changing
        }
    }
}

在Android manifest文件中加入以下内容,允许使用Wi-Fi设备上的硬件并声明应用程序正确支持了调用API所需的最低SDK版本.

在使用Wi-Fi Direct API之前,首先要确保应用程序能够访问硬件,并且设备支持Wi-Fi Direct协议。如果这些条件都满足,就可以获取一个WifiP2pManager实例,创建并注册广播接收器,最后就是使用Wi-Fi Direct API了。

<uses-sdk android:minSdkVersion="14"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

检查Wi-Fi Direct支持并已开启。推荐在广播接收器收到WIFI_P2P_STATE_CHANGED_ACTION意向的时候进行检测。检测结果需要通告相应的活动并做出处理:

@Override
public void onReceive(Context context, Intent intent) {
 
    String action = intent.getAction();
    if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
        int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
        if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
            // Wifi Direct is enabled
        } else {
            // Wi-Fi Direct is not enabled
        }
    }
}

在活动的onCreate()方法中获取WifiP2pManager对象的一个实例,通过该对象的initialize()方法向Wi-Fi Direct系统注册当前的应用程序。注册成功后,会返回一个WifiP2pManager.Channel,通过它,应用程序就能和Wi-Fi Direct系统交互。WifiP2pManager和WifiP2pManager.Channel对象以及一个活动的引用最后都被作为参数传递给自定义的广播接收器。这样,该活动就能够响应广播接收器的通知并作出相应的更新。当然,这样做也使程序具备了操纵设备Wi-Fi状态的能力:

WifiP2pManager mManager;
Channel mChannel;
BroadcastReceiver mReceiver;
@Override
protected void onCreate(Bundle savedInstanceState){
    mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    mChannel = mManager.initialize(this, getMainLooper(), null);
    mReceiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
}

创建一个意向过滤器(intent filter),其中添加的意向种类和广播接收器中的保持一致

IntentFilter mIntentFilter;
@Override
protected void onCreate(Bundle savedInstanceState){
    mIntentFilter = new IntentFilter();
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
}

在活动的onResume()方法中注册广播接收器,并在活动的onPause()方法中注销它:

/* register the broadcast receiver with the intent values to be matched */
@Override
protected void onResume() {
    super.onResume();
    registerReceiver(mReceiver, mIntentFilter);
}
/* unregister the broadcast receiver */
@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(mReceiver);
}

一旦成功获取WifiP2pManager.Channel并创建了广播接收器,应用程序就已经具备了使用Wi-Fi Direct相关函数和接收Wi-Fi Direct意向的能力。尽管放手使用WifiP2pManager为你提供的方法,让程序也拥有Wi-Fi Direct的特殊能力吧!

探测对等设备(Discovering peers)

调用discoverPeers()函数可以探测到有效距离内的对等设备。它是一个异步函数,调用成功与否会在程序所创建WifiP2pManager.ActionListener监听器的onSuccess()和onFailure()中给出通知。值得注意的是,onSuccess()方法只会对成功探测到对等设备这一事件做出通知,而并不会提供任何关于已发现的对等设备的具体信息

manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        ...
    }
 
    @Override
    public void onFailure(int reasonCode) {
        ...
    }
});

传输数据(Transferring data)

连接一旦建立成功,数据传输也就是顺理成章的事情。以下是通过socket发送数据的基本步骤:

创建ServerSocket。它将被用于监听特定端口,等待客户端发起的连接请求。该操作需要在后台线程中实现。
创建客户端Socket。客户端通过ServerSocket对应的IP和端口连接到服务设备。
客户端向服务器发生数据。客户socket成功连接到服务socket后,就能以字节流的形式向服务器发生数据了。
服务器socket通过accept()方法等待客户端数据连接的到来。该方法在收到客户端数据之前一直处于阻塞状态。因此,需要在单独的线程中调用它。数据连接一旦建立,服务设备就能接收到客户端的数据。这时要做的就是施以相应的动作,例如将数据保存到文件,或者是直接显示到用户界面上,等等。

结语

对于iOS和Android的内网、蓝牙直连功能,使用起来确实很方便, 基本上没有延时。不过遗憾的是,目前还没有一种通用的技术去使Android和iOS之间直连。对于文中诉述的MultipeerConnectivity和Wifi Connect技术, 我已封装成sdk,上传到github, 后续还会考虑在Unity的Asset Store上架,方便更多的读者使用。

参考文献: